mirror of
https://github.com/ppy/osu.git
synced 2024-11-13 16:13:34 +08:00
Merge branch 'master' into mania-key-count-mod-query
This commit is contained in:
commit
cb82fb0487
12
.github/workflows/diffcalc.yml
vendored
12
.github/workflows/diffcalc.yml
vendored
@ -126,7 +126,7 @@ jobs:
|
||||
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||
steps:
|
||||
- name: Create comment
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
message: |
|
||||
@ -253,7 +253,7 @@ jobs:
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
@ -284,7 +284,7 @@ jobs:
|
||||
|
||||
- name: Restore cache
|
||||
id: restore-cache
|
||||
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
|
||||
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
|
||||
with:
|
||||
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||
@ -358,7 +358,7 @@ jobs:
|
||||
steps:
|
||||
- name: Update comment on success
|
||||
if: ${{ needs.generator.result == 'success' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
@ -369,7 +369,7 @@ jobs:
|
||||
|
||||
- name: Update comment on failure
|
||||
if: ${{ needs.generator.result == 'failure' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: upsert
|
||||
@ -379,7 +379,7 @@ jobs:
|
||||
|
||||
- name: Update comment on cancellation
|
||||
if: ${{ needs.generator.result == 'cancelled' }}
|
||||
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
|
||||
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
|
||||
with:
|
||||
comment_tag: ${{ env.EXECUTION_ID }}
|
||||
mode: delete
|
||||
|
@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (circle1 is null || circle2 is null || slider is null)
|
||||
if (circle1 == null || circle2 == null || slider == null)
|
||||
return false;
|
||||
|
||||
var controlPoints = slider.Path.ControlPoints;
|
||||
@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (slider1 is null || slider2 is null || slider1Path is null)
|
||||
if (slider1 == null || slider2 == null || slider1Path == null)
|
||||
return false;
|
||||
|
||||
var controlPoints1 = slider1Path.ControlPoints;
|
||||
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddAssert("slider end is at same completion for last slider", () =>
|
||||
{
|
||||
if (slider1Path is null || slider2 is null)
|
||||
if (slider1Path == null || slider2 == null)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
@ -231,6 +231,137 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
(pos: circle2.Position, pathType: null)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMergeSliderSliderSameStartTime()
|
||||
{
|
||||
Slider? slider1 = null;
|
||||
SliderPath? slider1Path = null;
|
||||
Slider? slider2 = null;
|
||||
|
||||
AddStep("select two sliders", () =>
|
||||
{
|
||||
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
|
||||
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
|
||||
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
|
||||
EditorClock.Seek(slider1.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
|
||||
});
|
||||
|
||||
AddStep("move sliders to the same start time", () =>
|
||||
{
|
||||
slider2!.StartTime = slider1!.StartTime;
|
||||
});
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (slider1 == null || slider2 == null || slider1Path == null)
|
||||
return false;
|
||||
|
||||
var controlPoints1 = slider1Path.ControlPoints;
|
||||
var controlPoints2 = slider2.Path.ControlPoints;
|
||||
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
|
||||
|
||||
for (int i = 0; i < controlPoints1.Count - 1; i++)
|
||||
{
|
||||
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
|
||||
}
|
||||
|
||||
for (int i = 0; i < controlPoints2.Count; i++)
|
||||
{
|
||||
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
|
||||
}
|
||||
|
||||
return sliderCreatedFor(args);
|
||||
});
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddAssert("merged slider matches first slider", () =>
|
||||
{
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
|
||||
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
|
||||
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
|
||||
});
|
||||
|
||||
AddAssert("slider end is at same completion for last slider", () =>
|
||||
{
|
||||
if (slider1Path == null || slider2 == null)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMergeSliderSliderSameStartAndEndTime()
|
||||
{
|
||||
Slider? slider1 = null;
|
||||
SliderPath? slider1Path = null;
|
||||
Slider? slider2 = null;
|
||||
|
||||
AddStep("select two sliders", () =>
|
||||
{
|
||||
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
|
||||
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
|
||||
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
|
||||
EditorClock.Seek(slider1.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
|
||||
});
|
||||
|
||||
AddStep("move sliders to the same start & end time", () =>
|
||||
{
|
||||
slider2!.StartTime = slider1!.StartTime;
|
||||
slider2.Path = slider1.Path;
|
||||
});
|
||||
|
||||
mergeSelection();
|
||||
|
||||
AddAssert("slider created", () =>
|
||||
{
|
||||
if (slider1 == null || slider2 == null || slider1Path == null)
|
||||
return false;
|
||||
|
||||
var controlPoints1 = slider1Path.ControlPoints;
|
||||
var controlPoints2 = slider2.Path.ControlPoints;
|
||||
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
|
||||
|
||||
for (int i = 0; i < controlPoints1.Count - 1; i++)
|
||||
{
|
||||
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
|
||||
}
|
||||
|
||||
for (int i = 0; i < controlPoints2.Count; i++)
|
||||
{
|
||||
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
|
||||
}
|
||||
|
||||
return sliderCreatedFor(args);
|
||||
});
|
||||
|
||||
AddAssert("samples exist", sliderSampleExist);
|
||||
|
||||
AddAssert("merged slider matches first slider", () =>
|
||||
{
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
|
||||
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
|
||||
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
|
||||
});
|
||||
|
||||
AddAssert("slider end is at same completion for last slider", () =>
|
||||
{
|
||||
if (slider1Path == null || slider2 == null)
|
||||
return false;
|
||||
|
||||
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
|
||||
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
|
||||
});
|
||||
}
|
||||
|
||||
private void mergeSelection()
|
||||
{
|
||||
AddStep("merge selection", () =>
|
||||
|
@ -172,6 +172,54 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointPathType(4, null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStackingUpdatesPointsPosition()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
Vector2[] points =
|
||||
[
|
||||
new Vector2(200),
|
||||
new Vector2(300),
|
||||
new Vector2(500, 300),
|
||||
new Vector2(700, 200),
|
||||
new Vector2(500, 100)
|
||||
];
|
||||
|
||||
foreach (var point in points) addControlPointStep(point);
|
||||
|
||||
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 1);
|
||||
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
addAssertPointPositionChanged(points, i);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStackingUpdatesConnectionPosition()
|
||||
{
|
||||
createVisualiser(true);
|
||||
|
||||
Vector2 connectionPosition;
|
||||
addControlPointStep(connectionPosition = new Vector2(300));
|
||||
addControlPointStep(new Vector2(600));
|
||||
|
||||
// Apply a big number in stacking so the person running the test can clearly see if it fails
|
||||
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 10);
|
||||
|
||||
AddAssert($"Connection at {connectionPosition} changed",
|
||||
() => visualiser.Connections[0].Position,
|
||||
() => !Is.EqualTo(connectionPosition)
|
||||
);
|
||||
}
|
||||
|
||||
private void addAssertPointPositionChanged(Vector2[] points, int index)
|
||||
{
|
||||
AddAssert($"Point at {points.ElementAt(index)} changed",
|
||||
() => visualiser.Pieces[index].Position,
|
||||
() => !Is.EqualTo(points.ElementAt(index))
|
||||
);
|
||||
}
|
||||
|
||||
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
|
@ -24,14 +24,38 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestHotkeyHandling()
|
||||
{
|
||||
AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
|
||||
AddStep("deselect everything", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
|
||||
AddUntilStep("no popover present", getPopover, () => Is.Null);
|
||||
|
||||
AddStep("select single circle",
|
||||
() => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
|
||||
AddAssert("only playfield centre origin rotation available", () =>
|
||||
{
|
||||
var popover = getPopover();
|
||||
var buttons = popover.ChildrenOfType<EditorRadioButton>();
|
||||
return buttons.Any(btn => btn.Text == "Selection centre" && !btn.Enabled.Value)
|
||||
&& buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
|
||||
});
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("no popover present", getPopover, () => Is.Null);
|
||||
|
||||
AddStep("select first three objects", () =>
|
||||
{
|
||||
@ -44,14 +68,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.EqualTo(1));
|
||||
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
|
||||
AddAssert("both origin rotation available", () =>
|
||||
{
|
||||
var popover = getPopover();
|
||||
var buttons = popover.ChildrenOfType<EditorRadioButton>();
|
||||
return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value)
|
||||
&& buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
|
||||
});
|
||||
AddStep("press rotate hotkey", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.R);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
|
||||
AddUntilStep("no popover present", getPopover, () => Is.Null);
|
||||
|
||||
PreciseRotationPopover? getPopover() => this.ChildrenOfType<PreciseRotationPopover>().SingleOrDefault();
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddStep("add hitsounds", () =>
|
||||
{
|
||||
if (slider is null) return;
|
||||
if (slider == null) return;
|
||||
|
||||
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70);
|
||||
slider.Samples.Add(sample.With());
|
||||
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"move mouse to control point {index}", () =>
|
||||
{
|
||||
if (slider is null || visualiser is null) return;
|
||||
if (slider == null || visualiser == null) return;
|
||||
|
||||
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
|
||||
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position));
|
||||
@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
||||
{
|
||||
if (visualiser is null) return;
|
||||
if (visualiser == null) return;
|
||||
|
||||
MenuItem? item = visualiser.ContextMenuItems?.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||
|
||||
|
@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private IBindable<Vector2> hitObjectPosition;
|
||||
private IBindable<int> pathVersion;
|
||||
private IBindable<int> stackHeight;
|
||||
|
||||
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
|
||||
{
|
||||
@ -56,6 +57,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
pathVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
|
||||
|
||||
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
|
||||
stackHeight.BindValueChanged(_ => updateConnectingPath());
|
||||
|
||||
updateConnectingPath();
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private IBindable<Vector2> hitObjectPosition;
|
||||
private IBindable<float> hitObjectScale;
|
||||
private IBindable<int> stackHeight;
|
||||
|
||||
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
|
||||
{
|
||||
@ -105,6 +106,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
hitObjectScale = hitObject.ScaleBindable.GetBoundCopy();
|
||||
hitObjectScale.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
|
||||
stackHeight.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
IsSelected.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
updateMarkerDisplay();
|
||||
|
@ -311,7 +311,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
foreach (var splitPoint in controlPointsToSplitAt)
|
||||
{
|
||||
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null)
|
||||
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type == null)
|
||||
continue;
|
||||
|
||||
// Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider.
|
||||
|
@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private void updateState()
|
||||
{
|
||||
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||
CanRotate.Value = quad.Width > 0 || quad.Height > 0;
|
||||
CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
|
||||
CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any();
|
||||
}
|
||||
|
||||
private OsuHitObject[]? objectsInRotation;
|
||||
|
@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private SliderWithTextBoxInput<float> angleInput = null!;
|
||||
private EditorRadioButtonCollection rotationOrigin = null!;
|
||||
|
||||
private RadioButton selectionCentreButton = null!;
|
||||
|
||||
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
|
||||
{
|
||||
this.rotationHandler = rotationHandler;
|
||||
@ -59,13 +61,17 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
new RadioButton("Playfield centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
|
||||
new RadioButton("Selection centre",
|
||||
selectionCentreButton = new RadioButton("Selection centre",
|
||||
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
selectionCentreButton.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based rotation." : string.Empty;
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -76,6 +82,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
|
||||
rotationOrigin.Items.First().Select();
|
||||
|
||||
rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e =>
|
||||
{
|
||||
selectionCentreButton.Selected.Disabled = !e.NewValue;
|
||||
}, true);
|
||||
|
||||
rotationInfo.BindValueChanged(rotation =>
|
||||
{
|
||||
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
|
||||
|
@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private EditorToolButton rotateButton = null!;
|
||||
|
||||
private Bindable<bool> canRotatePlayfieldOrigin = null!;
|
||||
private Bindable<bool> canRotateSelectionOrigin = null!;
|
||||
|
||||
public SelectionRotationHandler RotationHandler { get; init; } = null!;
|
||||
|
||||
public TransformToolboxGroup()
|
||||
@ -51,9 +54,20 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
// aggregate two values into canRotate
|
||||
canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy();
|
||||
canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
||||
|
||||
canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy();
|
||||
canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate());
|
||||
|
||||
void updateCanRotateAggregate()
|
||||
{
|
||||
canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value;
|
||||
}
|
||||
|
||||
// bindings to `Enabled` on the buttons are decoupled on purpose
|
||||
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
|
||||
canRotate.BindTo(RotationHandler.CanRotate);
|
||||
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
InternalChild = textureAnimation = createTextureAnimation(state).With(animation =>
|
||||
{
|
||||
animation.Origin = animation.Anchor = Anchor.BottomLeft;
|
||||
animation.Scale = new Vector2(0.51f); // close enough to stable
|
||||
// matches stable (https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/Rulesets/Taiko/TaikoMascot.cs#L34)
|
||||
animation.Scale = new Vector2(0.6f);
|
||||
});
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
@ -77,6 +78,7 @@ namespace osu.Game.Tests.Database
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
|
||||
using (new RealmRulesetStore(realm, storage))
|
||||
{
|
||||
var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
|
||||
var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);
|
||||
|
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
this.getTargetContainer = getTargetContainer;
|
||||
|
||||
CanRotate.Value = true;
|
||||
CanRotateSelectionOrigin.Value = true;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Login;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -72,9 +73,24 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
|
||||
assertAPIState(APIState.Online);
|
||||
assertDropdownState(UserAction.Online);
|
||||
|
||||
AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); });
|
||||
AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); });
|
||||
|
||||
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
|
||||
|
||||
assertDropdownState(UserAction.Online);
|
||||
AddStep("change user state", () => dummyAPI.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb);
|
||||
assertDropdownState(UserAction.DoNotDisturb);
|
||||
}
|
||||
|
||||
private void assertDropdownState(UserAction state)
|
||||
{
|
||||
AddAssert($"dropdown state is {state}", () => loginOverlay.ChildrenOfType<UserDropdown>().First().Current.Value, () => Is.EqualTo(state));
|
||||
}
|
||||
|
||||
private void assertAPIState(APIState expected) =>
|
||||
|
@ -14,13 +14,16 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Ranking.Expanded;
|
||||
using osu.Game.Screens.Ranking.Expanded.Statistics;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
@ -67,6 +70,40 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
AddAssert("play time displayed", () => this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPPShownAsProvisionalWhenBeatmapHasNoLeaderboard()
|
||||
{
|
||||
AddStep("show example score", () =>
|
||||
{
|
||||
var beatmap = createTestBeatmap(new RealmUser());
|
||||
beatmap.Status = BeatmapOnlineStatus.Graveyard;
|
||||
showPanel(TestResources.CreateTestScoreInfo(beatmap));
|
||||
});
|
||||
|
||||
AddAssert("pp display faded out", () =>
|
||||
{
|
||||
var ppDisplay = this.ChildrenOfType<PerformanceStatistic>().Single();
|
||||
return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedBeatmaps;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPPShownAsProvisionalWhenUnrankedModsArePresent()
|
||||
{
|
||||
AddStep("show example score", () =>
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser()));
|
||||
score.Mods = score.Mods.Append(new OsuModDifficultyAdjust()).ToArray();
|
||||
showPanel(score);
|
||||
});
|
||||
|
||||
AddAssert("pp display faded out", () =>
|
||||
{
|
||||
var ppDisplay = this.ChildrenOfType<PerformanceStatistic>().Single();
|
||||
return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedMods;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWithDefaultDate()
|
||||
{
|
||||
|
@ -434,6 +434,9 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
if (!beatmaps.Any())
|
||||
throw new ArgumentException("No valid beatmap files found in the beatmap archive.");
|
||||
|
||||
return beatmaps;
|
||||
}
|
||||
}
|
||||
|
41
osu.Game/Graphics/Sprites/GlowingDrawable.cs
Normal file
41
osu.Game/Graphics/Sprites/GlowingDrawable.cs
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
public abstract partial class GlowingDrawable : BufferedContainer
|
||||
{
|
||||
// Inflate draw quad to prevent glow from trimming at the edges.
|
||||
// Padding won't suffice since it will affect drawable position in cases when it's not centered.
|
||||
protected override Quad ComputeScreenSpaceDrawQuad()
|
||||
=> base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(new Vector2(Blur.KernelSize(BlurSigma.X), Blur.KernelSize(BlurSigma.Y)));
|
||||
|
||||
public ColourInfo GlowColour
|
||||
{
|
||||
get => EffectColour;
|
||||
set
|
||||
{
|
||||
EffectColour = value;
|
||||
BackgroundColour = value.MultiplyAlpha(0f);
|
||||
}
|
||||
}
|
||||
|
||||
protected GlowingDrawable()
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
RedrawOnScale = false;
|
||||
DrawOriginal = true;
|
||||
Child = CreateDrawable();
|
||||
}
|
||||
|
||||
protected abstract Drawable CreateDrawable();
|
||||
}
|
||||
}
|
@ -4,24 +4,17 @@
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
public partial class GlowingSpriteText : BufferedContainer, IHasText
|
||||
public partial class GlowingSpriteText : GlowingDrawable, IHasText
|
||||
{
|
||||
private const float blur_sigma = 3f;
|
||||
|
||||
// Inflate draw quad to prevent glow from trimming at the edges.
|
||||
// Padding won't suffice since it will affect text position in cases when it's not centered.
|
||||
protected override Quad ComputeScreenSpaceDrawQuad() => base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(Blur.KernelSize(blur_sigma));
|
||||
|
||||
private readonly OsuSpriteText text;
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
public LocalisableString Text
|
||||
{
|
||||
@ -47,16 +40,6 @@ namespace osu.Game.Graphics.Sprites
|
||||
set => text.Colour = value;
|
||||
}
|
||||
|
||||
public ColourInfo GlowColour
|
||||
{
|
||||
get => EffectColour;
|
||||
set
|
||||
{
|
||||
EffectColour = value;
|
||||
BackgroundColour = value.MultiplyAlpha(0f);
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 Spacing
|
||||
{
|
||||
get => text.Spacing;
|
||||
@ -76,20 +59,16 @@ namespace osu.Game.Graphics.Sprites
|
||||
}
|
||||
|
||||
public GlowingSpriteText()
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
BlurSigma = new Vector2(blur_sigma);
|
||||
RedrawOnScale = false;
|
||||
DrawOriginal = true;
|
||||
EffectBlending = BlendingParameters.Additive;
|
||||
EffectPlacement = EffectPlacement.InFront;
|
||||
Child = text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
};
|
||||
}
|
||||
|
||||
protected override Drawable CreateDrawable() => text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
24
osu.Game/Localisation/ResultsScreenStrings.cs
Normal file
24
osu.Game/Localisation/ResultsScreenStrings.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class ResultsScreenStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.ResultsScreen";
|
||||
|
||||
/// <summary>
|
||||
/// "Performance points are not granted for this score because the beatmap is not ranked."
|
||||
/// </summary>
|
||||
public static LocalisableString NoPPForUnrankedBeatmaps => new TranslatableString(getKey(@"no_pp_for_unranked_beatmaps"), @"Performance points are not granted for this score because the beatmap is not ranked.");
|
||||
|
||||
/// <summary>
|
||||
/// "Performance points are not granted for this score because of unranked mods."
|
||||
/// </summary>
|
||||
public static LocalisableString NoPPForUnrankedMods => new TranslatableString(getKey(@"no_pp_for_unranked_mods"), @"Performance points are not granted for this score because of unranked mods.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -678,16 +678,21 @@ namespace osu.Game
|
||||
/// <summary>
|
||||
/// Allows a maximum of one unhandled exception, per second of execution.
|
||||
/// </summary>
|
||||
private bool onExceptionThrown(Exception _)
|
||||
/// <returns>Whether to ignore the exception and continue running.</returns>
|
||||
private bool onExceptionThrown(Exception ex)
|
||||
{
|
||||
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
|
||||
|
||||
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
|
||||
if (Interlocked.Decrement(ref allowableExceptions) < 0)
|
||||
{
|
||||
Logger.Log("Too many unhandled exceptions, crashing out.");
|
||||
RulesetStore.TryDisableCustomRulesetsCausing(ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Log($"Unhandled exception has been allowed with {allowableExceptions} more allowable exceptions.");
|
||||
// restore the stock of allowable exceptions after a short delay.
|
||||
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
|
||||
|
||||
return continueExecution;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
@ -30,15 +31,17 @@ namespace osu.Game.Overlays.Login
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private UserDropdown dropdown = null!;
|
||||
private UserDropdown? dropdown;
|
||||
|
||||
/// <summary>
|
||||
/// Called to request a hide of a parent displaying this container.
|
||||
/// </summary>
|
||||
public Action? RequestHide;
|
||||
|
||||
private IBindable<APIUser> user = null!;
|
||||
private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>();
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private readonly Bindable<UserStatus?> userStatus = new Bindable<UserStatus?>();
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
@ -61,11 +64,21 @@ namespace osu.Game.Overlays.Login
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
apiState.BindTo(api.State);
|
||||
apiState.BindValueChanged(onlineStateChanged, true);
|
||||
|
||||
user = api.LocalUser.GetBoundCopy();
|
||||
user.BindValueChanged(u =>
|
||||
{
|
||||
status.UnbindBindings();
|
||||
status.BindTo(u.NewValue.Status);
|
||||
}, true);
|
||||
|
||||
status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true);
|
||||
}
|
||||
|
||||
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
|
||||
@ -144,9 +157,6 @@ namespace osu.Game.Overlays.Login
|
||||
},
|
||||
};
|
||||
|
||||
userStatus.BindTo(api.LocalUser.Value.Status);
|
||||
userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true);
|
||||
|
||||
dropdown.Current.BindValueChanged(action =>
|
||||
{
|
||||
switch (action.NewValue)
|
||||
@ -171,6 +181,7 @@ namespace osu.Game.Overlays.Login
|
||||
break;
|
||||
}
|
||||
}, true);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@ -180,6 +191,9 @@ namespace osu.Game.Overlays.Login
|
||||
|
||||
private void updateDropdownCurrent(UserStatus? status)
|
||||
{
|
||||
if (dropdown == null)
|
||||
return;
|
||||
|
||||
switch (status)
|
||||
{
|
||||
case UserStatus.Online:
|
||||
|
@ -781,7 +781,7 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </remarks>>
|
||||
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||
{
|
||||
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null)
|
||||
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton == null)
|
||||
return false;
|
||||
|
||||
SelectAllModsButton.TriggerClick();
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
CanRotate.Value = selectedItems.Count > 0;
|
||||
CanRotateSelectionOrigin.Value = selectedItems.Count > 0;
|
||||
}
|
||||
|
||||
private Drawable[]? objectsInRotation;
|
||||
|
@ -212,6 +212,14 @@ namespace osu.Game.Rulesets.Edit
|
||||
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
|
||||
.ToList();
|
||||
|
||||
foreach (var item in toolboxCollection.Items)
|
||||
{
|
||||
item.Selected.DisabledChanged += isDisabled =>
|
||||
{
|
||||
item.TooltipText = isDisabled ? "Add at least one timing point first!" : string.Empty;
|
||||
};
|
||||
}
|
||||
|
||||
TernaryStates = CreateTernaryButtons().ToArray();
|
||||
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
|
||||
|
||||
@ -244,6 +252,14 @@ namespace osu.Game.Rulesets.Edit
|
||||
if (!timing.NewValue)
|
||||
setSelectTool();
|
||||
});
|
||||
|
||||
EditorBeatmap.HasTiming.BindValueChanged(hasTiming =>
|
||||
{
|
||||
foreach (var item in toolboxCollection.Items)
|
||||
{
|
||||
item.Selected.Disabled = !hasTiming.NewValue;
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -347,7 +347,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
// Edge-case rules (to match stable).
|
||||
if (type == PathType.PERFECT_CURVE)
|
||||
{
|
||||
int endPointLength = endPoint is null ? 0 : 1;
|
||||
int endPointLength = endPoint == null ? 0 : 1;
|
||||
|
||||
if (vertices.Length + endPointLength != 3)
|
||||
type = PathType.BEZIER;
|
||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
|
||||
public bool Equals(PathControlPoint other) => Position == other?.Position && Type == other.Type;
|
||||
|
||||
public override string ToString() => type is null
|
||||
public override string ToString() => type == null
|
||||
? $"Position={Position}"
|
||||
: $"Position={Position}, Type={type}";
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
var controlPoints = sliderPath.ControlPoints;
|
||||
|
||||
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type is null).ToList();
|
||||
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type == null).ToList();
|
||||
|
||||
// Inherited points after a linear point, as well as the first control point if it inherited,
|
||||
// should be treated as linear points, so their types are temporarily changed to linear.
|
||||
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
inheritedLinearPoints.ForEach(p => p.Type = null);
|
||||
|
||||
// Recalculate middle perfect curve control points at the end of the slider path.
|
||||
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type is null && segmentEnds.Any())
|
||||
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type == null && segmentEnds.Any())
|
||||
{
|
||||
double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
|
||||
double lastSegmentEnd = segmentEnds[^1];
|
||||
|
@ -3,8 +3,11 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
@ -13,17 +16,20 @@ namespace osu.Game.Rulesets
|
||||
{
|
||||
public class RealmRulesetStore : RulesetStore
|
||||
{
|
||||
private readonly RealmAccess realmAccess;
|
||||
public override IEnumerable<RulesetInfo> AvailableRulesets => availableRulesets;
|
||||
|
||||
private readonly List<RulesetInfo> availableRulesets = new List<RulesetInfo>();
|
||||
|
||||
public RealmRulesetStore(RealmAccess realm, Storage? storage = null)
|
||||
public RealmRulesetStore(RealmAccess realmAccess, Storage? storage = null)
|
||||
: base(storage)
|
||||
{
|
||||
prepareDetachedRulesets(realm);
|
||||
this.realmAccess = realmAccess;
|
||||
prepareDetachedRulesets();
|
||||
informUserAboutBrokenRulesets();
|
||||
}
|
||||
|
||||
private void prepareDetachedRulesets(RealmAccess realmAccess)
|
||||
private void prepareDetachedRulesets()
|
||||
{
|
||||
realmAccess.Write(realm =>
|
||||
{
|
||||
@ -143,5 +149,48 @@ namespace osu.Game.Rulesets
|
||||
|
||||
instance.CreateBeatmapProcessor(converter.Convert());
|
||||
}
|
||||
|
||||
private void informUserAboutBrokenRulesets()
|
||||
{
|
||||
if (RulesetStorage == null)
|
||||
return;
|
||||
|
||||
foreach (string brokenRulesetDll in RulesetStorage.GetFiles(@".", @"*.dll.broken"))
|
||||
{
|
||||
Logger.Log($"Ruleset '{Path.GetFileNameWithoutExtension(brokenRulesetDll)}' has been disabled due to causing a crash.\n\n"
|
||||
+ "Please update the ruleset or report the issue to the developers of the ruleset if no updates are available.", level: LogLevel.Important);
|
||||
}
|
||||
}
|
||||
|
||||
internal void TryDisableCustomRulesetsCausing(Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stackTrace = new StackTrace(exception);
|
||||
|
||||
foreach (var frame in stackTrace.GetFrames())
|
||||
{
|
||||
var declaringAssembly = frame.GetMethod()?.DeclaringType?.Assembly;
|
||||
if (declaringAssembly == null)
|
||||
continue;
|
||||
|
||||
if (UserRulesetAssemblies.Contains(declaringAssembly))
|
||||
{
|
||||
string sourceLocation = declaringAssembly.Location;
|
||||
string destinationLocation = Path.ChangeExtension(sourceLocation, @".dll.broken");
|
||||
|
||||
if (File.Exists(sourceLocation))
|
||||
{
|
||||
Logger.Log($"Unhandled exception traced back to custom ruleset {Path.GetFileNameWithoutExtension(sourceLocation)}. Marking as broken.");
|
||||
File.Move(sourceLocation, destinationLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"Attempt to trace back crash to custom ruleset failed: {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ namespace osu.Game.Rulesets
|
||||
private const string ruleset_library_prefix = @"osu.Game.Rulesets";
|
||||
|
||||
protected readonly Dictionary<Assembly, Type> LoadedAssemblies = new Dictionary<Assembly, Type>();
|
||||
protected readonly HashSet<Assembly> UserRulesetAssemblies = new HashSet<Assembly>();
|
||||
protected readonly Storage? RulesetStorage;
|
||||
|
||||
/// <summary>
|
||||
/// All available rulesets.
|
||||
@ -41,9 +43,9 @@ namespace osu.Game.Rulesets
|
||||
// to load as unable to locate the game core assembly.
|
||||
AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly;
|
||||
|
||||
var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
|
||||
if (rulesetStorage != null)
|
||||
loadUserRulesets(rulesetStorage);
|
||||
RulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
|
||||
if (RulesetStorage != null)
|
||||
loadUserRulesets(RulesetStorage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -105,7 +107,11 @@ namespace osu.Game.Rulesets
|
||||
var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll");
|
||||
|
||||
foreach (string? ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
|
||||
loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
|
||||
{
|
||||
var assembly = loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
|
||||
if (assembly != null)
|
||||
UserRulesetAssemblies.Add(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFromDisk()
|
||||
@ -126,21 +132,25 @@ namespace osu.Game.Rulesets
|
||||
}
|
||||
}
|
||||
|
||||
private void loadRulesetFromFile(string file)
|
||||
private Assembly? loadRulesetFromFile(string file)
|
||||
{
|
||||
string filename = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
if (LoadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
|
||||
return;
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
addRuleset(Assembly.LoadFrom(file));
|
||||
var assembly = Assembly.LoadFrom(file);
|
||||
addRuleset(assembly);
|
||||
return assembly;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogFailedLoad(filename, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addRuleset(Assembly assembly)
|
||||
|
@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.UI
|
||||
modAcronym.Text = value.Acronym;
|
||||
modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question;
|
||||
|
||||
if (value.Icon is null)
|
||||
if (value.Icon == null)
|
||||
{
|
||||
modIcon.FadeOut();
|
||||
modAcronym.FadeIn();
|
||||
|
@ -33,9 +33,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
|
||||
private Drawable icon = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
public EditorRadioButton(RadioButton button)
|
||||
{
|
||||
Button = button;
|
||||
@ -76,8 +73,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
Selected?.Invoke(Button);
|
||||
};
|
||||
|
||||
editorBeatmap?.HasTiming.BindValueChanged(hasTiming => Button.Selected.Disabled = !hasTiming.NewValue, true);
|
||||
|
||||
Button.Selected.BindDisabledChanged(disabled => Enabled.Value = !disabled, true);
|
||||
updateSelectionState();
|
||||
}
|
||||
@ -99,6 +94,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
X = 40f
|
||||
};
|
||||
|
||||
public LocalisableString TooltipText => Enabled.Value ? string.Empty : "Add at least one timing point first!";
|
||||
public LocalisableString TooltipText => Button.TooltipText;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
{
|
||||
@ -11,6 +12,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this <see cref="RadioButton"/> is selected.
|
||||
/// Disable this bindable to disable the button.
|
||||
/// </summary>
|
||||
public readonly BindableBool Selected;
|
||||
|
||||
@ -50,5 +52,8 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
/// Deselects this <see cref="RadioButton"/>.
|
||||
/// </summary>
|
||||
public void Deselect() => Selected.Value = false;
|
||||
|
||||
// Tooltip text that will be shown when hovered over
|
||||
public LocalisableString TooltipText { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -512,7 +512,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected virtual void OnBlueprintDeselected(SelectionBlueprint<T> blueprint)
|
||||
{
|
||||
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
|
||||
if (SelectionBlueprints.Contains(blueprint))
|
||||
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
|
||||
|
||||
SelectionHandler.HandleDeselected(blueprint);
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private void load()
|
||||
{
|
||||
if (rotationHandler != null)
|
||||
canRotate.BindTo(rotationHandler.CanRotate);
|
||||
canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin);
|
||||
|
||||
canRotate.BindValueChanged(_ => recreate(), true);
|
||||
}
|
||||
|
@ -13,9 +13,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
public partial class SelectionRotationHandler : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the rotation can currently be performed.
|
||||
/// Whether rotation anchored by the selection origin can currently be performed.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanRotate { get; private set; } = new BindableBool();
|
||||
public Bindable<bool> CanRotateSelectionOrigin { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Whether rotation anchored by the center of the playfield can currently be performed.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanRotatePlayfieldOrigin { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a single, instant, atomic rotation operation.
|
||||
|
@ -1,52 +1,16 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.Break
|
||||
{
|
||||
public partial class BlurredIcon : BufferedContainer
|
||||
public partial class BlurredIcon : GlowIcon
|
||||
{
|
||||
private readonly SpriteIcon icon;
|
||||
|
||||
public IconUsage Icon
|
||||
{
|
||||
set => icon.Icon = value;
|
||||
get => icon.Icon;
|
||||
}
|
||||
|
||||
public override Vector2 Size
|
||||
{
|
||||
set
|
||||
{
|
||||
icon.Size = value;
|
||||
base.Size = value + BlurSigma * 5;
|
||||
ForceRedraw();
|
||||
}
|
||||
get => base.Size;
|
||||
}
|
||||
|
||||
public BlurredIcon()
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
Child = icon = new SpriteIcon
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Shadow = false,
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Colour = colours.BlueLighter;
|
||||
EffectBlending = BlendingParameters.Additive;
|
||||
DrawOriginal = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.Break
|
||||
|
||||
private const int blurred_icon_blur_sigma = 20;
|
||||
private const int blurred_icon_size = 130;
|
||||
private const float blurred_icon_final_offset = 0.35f;
|
||||
private const float blurred_icon_final_offset = 0.38f;
|
||||
private const float blurred_icon_offscreen_offset = 0.7f;
|
||||
|
||||
private readonly GlowIcon leftGlowIcon;
|
||||
|
@ -3,64 +3,45 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.Break
|
||||
{
|
||||
public partial class GlowIcon : Container
|
||||
public partial class GlowIcon : GlowingDrawable
|
||||
{
|
||||
private readonly SpriteIcon spriteIcon;
|
||||
private readonly BlurredIcon blurredIcon;
|
||||
|
||||
public override Vector2 Size
|
||||
{
|
||||
get => base.Size;
|
||||
set
|
||||
{
|
||||
blurredIcon.Size = spriteIcon.Size = value;
|
||||
blurredIcon.ForceRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 BlurSigma
|
||||
{
|
||||
get => blurredIcon.BlurSigma;
|
||||
set => blurredIcon.BlurSigma = value;
|
||||
}
|
||||
private SpriteIcon icon = null!;
|
||||
|
||||
public IconUsage Icon
|
||||
{
|
||||
get => spriteIcon.Icon;
|
||||
set => spriteIcon.Icon = blurredIcon.Icon = value;
|
||||
set => icon.Icon = value;
|
||||
get => icon.Icon;
|
||||
}
|
||||
|
||||
public new Vector2 Size
|
||||
{
|
||||
set => icon.Size = value;
|
||||
get => icon.Size;
|
||||
}
|
||||
|
||||
public GlowIcon()
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
blurredIcon = new BlurredIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
spriteIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
blurredIcon.Colour = colours.Blue;
|
||||
GlowColour = colours.BlueLighter;
|
||||
}
|
||||
|
||||
protected override Drawable CreateDrawable() => icon = new SpriteIcon
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Shadow = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -4,20 +4,26 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Ranking.Expanded.Statistics
|
||||
{
|
||||
public partial class PerformanceStatistic : StatisticDisplay
|
||||
public partial class PerformanceStatistic : StatisticDisplay, IHasTooltip
|
||||
{
|
||||
public LocalisableString TooltipText { get; private set; }
|
||||
|
||||
private readonly ScoreInfo score;
|
||||
|
||||
private readonly Bindable<int> performance = new Bindable<int>();
|
||||
@ -37,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
|
||||
{
|
||||
if (score.PP.HasValue)
|
||||
{
|
||||
setPerformanceValue(score.PP.Value);
|
||||
setPerformanceValue(score, score.PP.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -52,15 +58,33 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
|
||||
|
||||
var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false);
|
||||
|
||||
Schedule(() => setPerformanceValue(result.Total));
|
||||
Schedule(() => setPerformanceValue(score, result.Total));
|
||||
}, cancellationToken ?? default);
|
||||
}
|
||||
}
|
||||
|
||||
private void setPerformanceValue(double? pp)
|
||||
private void setPerformanceValue(ScoreInfo scoreInfo, double? pp)
|
||||
{
|
||||
if (pp.HasValue)
|
||||
{
|
||||
performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero);
|
||||
|
||||
if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints())
|
||||
{
|
||||
Alpha = 0.5f;
|
||||
TooltipText = ResultsScreenStrings.NoPPForUnrankedBeatmaps;
|
||||
}
|
||||
else if (scoreInfo.Mods.Any(m => !m.Ranked))
|
||||
{
|
||||
Alpha = 0.5f;
|
||||
TooltipText = ResultsScreenStrings.NoPPForUnrankedMods;
|
||||
}
|
||||
else
|
||||
{
|
||||
Alpha = 1f;
|
||||
TooltipText = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Appear()
|
||||
|
Loading…
Reference in New Issue
Block a user