1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-22 09:09:54 +08:00

Merge branch 'master' into screen-scaling-tablet-output

This commit is contained in:
Bartłomiej Dach
2025-12-09 08:05:20 +01:00
committed by GitHub
Unverified
137 changed files with 2124 additions and 1007 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1118.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1205.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private double placementStartTime;
private double placementEndTime;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
public BananaShowerPlacementBlueprint()
{
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
private InputManager inputManager = null!;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
public JuiceStreamPlacementBlueprint()
{
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
@@ -224,7 +225,8 @@ namespace osu.Game.Rulesets.Catch.Edit
#region Clipboard handling
public override string ConvertSelectionToString()
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<CatchHitObject>().OrderBy(h => h.StartTime)
.Select(h => (h.IndexInCurrentCombo + 1).ToString(CultureInfo.InvariantCulture)));
// 1,2,3,4 ...
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
@@ -5,6 +5,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
@@ -16,6 +17,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual;
using osuTK.Input;
@@ -36,21 +38,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestPlaceBeforeCurrentTimeDownwards()
{
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
AddStep("move mouse before current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("note start time < 0", () => getNote().StartTime < 0);
}
[Test]
public void TestPlaceAfterCurrentTimeDownwards()
{
AddStep("move mouse after current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
@@ -58,7 +47,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("note start time > 0", () => getNote().StartTime > 0);
AddAssert("note start time < 200", () => getNote().StartTime < 200);
}
[Test]
public void TestPlaceAfterCurrentTimeDownwards()
{
AddStep("seek to 200", () => HitObjectContainer.Dependencies.Get<EditorClock>().Seek(200));
AddStep("move mouse after current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(300));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("note start time > 200", () => getNote().StartTime > 200);
}
private Note getNote() => this.ChildrenOfType<DrawableNote>().FirstOrDefault()?.HitObject;
@@ -18,15 +18,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
public void TestNormalSelection()
{
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)>
{ (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) }
));
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, [(5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1)]));
addReset();
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)>
{ (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) }
));
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, [(42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1)]));
addReset();
AddStep("add notes to row", () =>
@@ -41,15 +37,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
EditorBeatmap.AddRange(new[] { second, third, forth });
});
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)>
{ (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) }
));
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, [(11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3)]));
addReset();
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)>
{ (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) }
));
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, [(96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1)]));
}
[Test]
public void TestRoundingToNearestMillisecondApplied()
{
AddStep("resnap note to have fractional coordinates",
() => EditorBeatmap.HitObjects.OfType<ManiaHitObject>().Single(ho => ho.StartTime == 85_373 && ho.Column == 1).StartTime = 85_373.125);
addStepClickLink("01:25:373 (85373|1)");
AddAssert("selected note", () => checkSnapAndSelectColumn(85_373.125, [(85_373.125, 1)]));
}
[Test]
@@ -75,7 +76,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
private void addReset() => addStepClickLink("00:00:000", "reset", false);
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null)
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(double, int)>? columnPairs = null)
{
bool checkColumns = columnPairs != null
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(HitObject.Duration, 0);
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(HitObject.Duration, 0));
public HoldNotePlacementBlueprint()
: base(new HoldNote())
@@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@@ -54,7 +56,8 @@ namespace osu.Game.Rulesets.Mania.Edit
};
public override string ConvertSelectionToString()
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime)
.Select(h => FormattableString.Invariant($"{Math.Round(h.StartTime)}|{h.Column}")));
// 123|0,456|1,789|2 ...
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
@@ -73,10 +76,10 @@ namespace osu.Game.Rulesets.Mania.Edit
if (split.Length != 2)
continue;
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
if (!int.TryParse(split[0], out int time) || !int.TryParse(split[1], out int column))
continue;
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => Precision.AlmostEquals(h.StartTime, time, 0.5) && h.Column == column);
if (current == null)
continue;
@@ -245,13 +245,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("grid spacing is distance to slider tail", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1)
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y);
});
AddAssert("grid rotation points to slider tail", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
return Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1);
});
AddStep("start grid placement", () => InputManager.Key(Key.Number5));
@@ -280,9 +280,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("grid spacing and rotation unchanged", () =>
{
var composer = Editor.ChildrenOfType<RectangularPositionSnapGrid>().Single();
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.01)
return Precision.AlmostEquals(composer.Spacing.Value.X, 32.05, 0.1)
&& Precision.AlmostEquals(composer.Spacing.Value.X, composer.Spacing.Value.Y)
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.01);
&& Precision.AlmostEquals(composer.GridLineRotation.Value, 0.09, 0.1);
});
}
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
@@ -22,7 +23,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[TestFixture]
public partial class TestSceneSliderDrawing : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new TestBeatmap(ruleset, false);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
return beatmap;
}
[Test]
public void TestTouchInputPlaceHitCircleDirectly()
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
{
this.gridToolboxGroup = gridToolboxGroup;
originalOrigin = gridToolboxGroup.StartPosition.Value;
originalSpacing = gridToolboxGroup.Spacing.Value;
originalSpacing = gridToolboxGroup.GridLineSpacing.Value;
originalRotation = gridToolboxGroup.GridLinesRotation.Value;
}
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
{
// Reset the grid to the default values.
gridToolboxGroup.StartPosition.Value = gridToolboxGroup.StartPosition.Default;
gridToolboxGroup.Spacing.Value = gridToolboxGroup.Spacing.Default;
gridToolboxGroup.GridLineSpacing.Value = gridToolboxGroup.GridLineSpacing.Default;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = gridToolboxGroup.GridLinesRotation.Default;
EndPlacement(true);
@@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
// Default to the original spacing and rotation if the distance is too small.
if (Vector2.Distance(gridToolboxGroup.StartPosition.Value, pos) < 2)
{
gridToolboxGroup.Spacing.Value = originalSpacing;
gridToolboxGroup.GridLineSpacing.Value = originalSpacing;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
}
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
private void resetGridState()
{
gridToolboxGroup.StartPosition.Value = originalOrigin;
gridToolboxGroup.Spacing.Value = originalSpacing;
gridToolboxGroup.GridLineSpacing.Value = originalSpacing;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
}
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
protected override bool IsValidForPlacement => HitObject.Path.HasValidLengthForPlacement;
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || HitObject.Path.HasValidLengthForPlacement);
public SliderPlacementBlueprint()
: base(new Slider())
@@ -5,8 +5,10 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
@@ -42,25 +44,31 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly BindableInt displayTolerance = new BindableInt(90)
{
MinValue = 5,
MaxValue = 100
MaxValue = 100,
Precision = 1,
};
private readonly BindableInt displayCornerThreshold = new BindableInt(40)
{
MinValue = 5,
MaxValue = 100
MaxValue = 100,
Precision = 1,
};
private readonly BindableInt displayCircleThreshold = new BindableInt(30)
{
MinValue = 0,
MaxValue = 100
MaxValue = 100,
Precision = 1,
};
private ExpandableSlider<int> toleranceSlider = null!;
private ExpandableSlider<int> cornerThresholdSlider = null!;
private ExpandableSlider<int> circleThresholdSlider = null!;
[Resolved]
private IExpandingContainer? expandingContainer { get; set; }
[BackgroundDependencyLoader]
private void load()
{
@@ -68,15 +76,18 @@ namespace osu.Game.Rulesets.Osu.Edit
{
toleranceSlider = new ExpandableSlider<int>
{
Current = displayTolerance
Current = displayTolerance,
ExpandedLabelText = "Control point spacing",
},
cornerThresholdSlider = new ExpandableSlider<int>
{
Current = displayCornerThreshold
Current = displayCornerThreshold,
ExpandedLabelText = "Corner bias",
},
circleThresholdSlider = new ExpandableSlider<int>
{
Current = displayCircleThreshold
Current = displayCircleThreshold,
ExpandedLabelText = "Perfect curve bias"
}
};
}
@@ -88,24 +99,18 @@ namespace osu.Game.Rulesets.Osu.Edit
displayTolerance.BindValueChanged(tolerance =>
{
toleranceSlider.ContractedLabelText = $"C. P. S.: {tolerance.NewValue:N0}";
toleranceSlider.ExpandedLabelText = $"Control Point Spacing: {tolerance.NewValue:N0}";
Tolerance.Value = displayToInternalTolerance(tolerance.NewValue);
}, true);
displayCornerThreshold.BindValueChanged(threshold =>
{
cornerThresholdSlider.ContractedLabelText = $"C. T.: {threshold.NewValue:N0}";
cornerThresholdSlider.ExpandedLabelText = $"Corner Threshold: {threshold.NewValue:N0}";
cornerThresholdSlider.ContractedLabelText = $"C. B.: {threshold.NewValue:N0}";
CornerThreshold.Value = displayToInternalCornerThreshold(threshold.NewValue);
}, true);
displayCircleThreshold.BindValueChanged(threshold =>
{
circleThresholdSlider.ContractedLabelText = $"P. C. T.: {threshold.NewValue:N0}";
circleThresholdSlider.ExpandedLabelText = $"Perfect Curve Threshold: {threshold.NewValue:N0}";
circleThresholdSlider.ContractedLabelText = $"P. C. B.: {threshold.NewValue:N0}";
CircleThreshold.Value = displayToInternalCircleThreshold(threshold.NewValue);
}, true);
@@ -119,6 +124,11 @@ namespace osu.Game.Rulesets.Osu.Edit
displayCircleThreshold.Value = internalToDisplayCircleThreshold(threshold.NewValue)
);
expandingContainer?.Expanded.BindValueChanged(v =>
{
Spacing = v.NewValue ? new Vector2(5) : new Vector2(15);
}, true);
float displayToInternalTolerance(float v) => v / 50f;
int internalToDisplayTolerance(float v) => (int)Math.Round(v * 50f);
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
MinValue = 0f,
MaxValue = OsuPlayfield.BASE_SIZE.X,
Precision = 0.01f,
Precision = 0.1f,
};
/// <summary>
@@ -48,17 +48,17 @@ namespace osu.Game.Rulesets.Osu.Edit
{
MinValue = 0f,
MaxValue = OsuPlayfield.BASE_SIZE.Y,
Precision = 0.01f,
Precision = 0.1f,
};
/// <summary>
/// The spacing between grid lines.
/// </summary>
public BindableFloat Spacing { get; } = new BindableFloat(4f)
public BindableFloat GridLineSpacing { get; } = new BindableFloat(4f)
{
MinValue = 4f,
MaxValue = 256f,
Precision = 0.01f,
Precision = 0.1f,
};
/// <summary>
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
MinValue = -180f,
MaxValue = 180f,
Precision = 0.01f,
Precision = 0.1f,
};
/// <summary>
@@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit
float dist = Vector2.Distance(point1, point2);
while (dist >= max_automatic_spacing)
dist /= 2;
Spacing.Value = dist;
GridLineSpacing.Value = dist;
}
[BackgroundDependencyLoader]
@@ -127,21 +127,25 @@ namespace osu.Game.Rulesets.Osu.Edit
{
Current = StartPositionX,
KeyboardStep = 1,
ExpandedLabelText = "X offset",
},
startPositionYSlider = new ExpandableSlider<float>
{
Current = StartPositionY,
KeyboardStep = 1,
ExpandedLabelText = "Y offset",
},
spacingSlider = new ExpandableSlider<float>
{
Current = Spacing,
Current = GridLineSpacing,
KeyboardStep = 1,
ExpandedLabelText = "Spacing",
},
gridLinesRotationSlider = new ExpandableSlider<float>
{
Current = GridLinesRotation,
KeyboardStep = 1,
ExpandedLabelText = "Rotation",
},
new FillFlowContainer
{
@@ -170,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Edit
},
};
Spacing.Value = editorBeatmap.GridSize;
GridLineSpacing.Value = editorBeatmap.GridSize;
}
protected override void LoadComplete()
@@ -182,14 +186,12 @@ namespace osu.Game.Rulesets.Osu.Edit
StartPositionX.BindValueChanged(x =>
{
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}";
startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:#,0.##}";
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
}, true);
StartPositionY.BindValueChanged(y =>
{
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}";
startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:#,0.##}";
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
}, true);
@@ -199,10 +201,9 @@ namespace osu.Game.Rulesets.Osu.Edit
StartPositionY.Value = pos.NewValue.Y;
});
Spacing.BindValueChanged(spacing =>
GridLineSpacing.BindValueChanged(spacing =>
{
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}";
SpacingVector.Value = new Vector2(spacing.NewValue);
editorBeatmap.GridSize = (int)spacing.NewValue;
}, true);
@@ -210,7 +211,6 @@ namespace osu.Game.Rulesets.Osu.Edit
GridLinesRotation.BindValueChanged(rotation =>
{
gridLinesRotationSlider.ContractedLabelText = $"R: {rotation.NewValue:#,0.##}";
gridLinesRotationSlider.ExpandedLabelText = $"Rotation: {rotation.NewValue:#,0.##}";
}, true);
GridType.BindValueChanged(v =>
@@ -239,6 +239,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
Spacing = v.NewValue ? new Vector2(5) : new Vector2(15);
}, true);
}
@@ -252,7 +254,7 @@ namespace osu.Game.Rulesets.Osu.Edit
switch (e.Action)
{
case GlobalAction.EditorCycleGridSpacing:
Spacing.Value = Spacing.Value * 2 >= max_automatic_spacing ? Spacing.Value / 8 : Spacing.Value * 2;
GridLineSpacing.Value = GridLineSpacing.Value * 2 >= max_automatic_spacing ? GridLineSpacing.Value / 8 : GridLineSpacing.Value * 2;
return true;
case GlobalAction.EditorCycleGridType:
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
@@ -142,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Edit
case PositionSnapGridType.Triangle:
var triangularPositionSnapGrid = new TriangularPositionSnapGrid();
triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
triangularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.GridLineSpacing);
triangularPositionSnapGrid.GridLineRotation.BindTo(OsuGridToolboxGroup.GridLinesRotation);
positionSnapGrid = triangularPositionSnapGrid;
@@ -151,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Edit
case PositionSnapGridType.Circle:
var circularPositionSnapGrid = new CircularPositionSnapGrid();
circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.Spacing);
circularPositionSnapGrid.Spacing.BindTo(OsuGridToolboxGroup.GridLineSpacing);
positionSnapGrid = circularPositionSnapGrid;
break;
@@ -171,7 +172,8 @@ namespace osu.Game.Rulesets.Osu.Edit
=> new OsuBlueprintContainer(this);
public override string ConvertSelectionToString()
=> string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
=> string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime)
.Select(h => (h.IndexInCurrentCombo + 1).ToString(CultureInfo.InvariantCulture)));
// 1,2,3,4 ...
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
@@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => @"Don't use the same key twice in a row!";
public override IconUsage? Icon => OsuIcon.ModAlternate;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
public override bool Ranked => true;
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
}
@@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override IconUsage? Icon => OsuIcon.ModSingleTap;
public override LocalisableString Description => @"You must only use one key!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
public override bool Ranked => true;
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
}
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Performance;
using osu.Game.Graphics;
@@ -12,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
{
public partial class CursorPathContainer : Path
public partial class CursorPathContainer : SmoothPath
{
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
private readonly SortedSet<AnalysisFrameEntry> aliveEntries = new SortedSet<AnalysisFrameEntry>(new AimLinePointComparator());
@@ -22,14 +21,13 @@ namespace osu.Game.Rulesets.Osu.UI.ReplayAnalysis
lifetimeManager.EntryBecameAlive += entryBecameAlive;
lifetimeManager.EntryBecameDead += entryBecameDead;
PathRadius = 0.5f;
PathRadius = 1f;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Pink2;
BackgroundColour = colours.Pink2.Opacity(0);
}
protected override void Update()
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
[Resolved]
private TaikoHitObjectComposer? composer { get; set; }
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0);
protected override bool IsValidForPlacement => base.IsValidForPlacement && (PlacementActive == PlacementState.Waiting || Precision.DefinitelyBigger(spanPlacementObject.Duration, 0));
public TaikoSpanPlacementBlueprint(HitObject hitObject)
: base(hitObject)
@@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Checks
protected override IEnumerable<(DifficultyRating rating, double thresholdMs, string name)> GetThresholds()
{
// See lowest difficulty requirements in https://osu.ppy.sh/wiki/en/Ranking_criteria/osu%21taiko#general
yield return (DifficultyRating.Hard, new TimeSpan(0, 3, 30).TotalMilliseconds, "Muzukashii");
yield return (DifficultyRating.Insane, new TimeSpan(0, 4, 15).TotalMilliseconds, "Oni");
yield return (DifficultyRating.Expert, new TimeSpan(0, 5, 0).TotalMilliseconds, "Inner Oni");
yield return (DifficultyRating.Hard, new TimeSpan(0, 2, 30).TotalMilliseconds, "Muzukashii");
yield return (DifficultyRating.Insane, new TimeSpan(0, 3, 15).TotalMilliseconds, "Oni");
yield return (DifficultyRating.Expert, new TimeSpan(0, 4, 0).TotalMilliseconds, "Inner Oni");
}
}
}
@@ -29,6 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override IconUsage? Icon => OsuIcon.ModSingleTap;
public override LocalisableString Description => @"One key for dons, one key for kats.";
public override bool Ranked => true;
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(TaikoModCinema) };
public override ModType Type => ModType.Conversion;
@@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override ModType Type => ModType.Conversion;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModRandom)).ToArray();
public override bool Ranked => true;
public void ApplyToBeatmap(IBeatmap beatmap)
{
+26 -1
View File
@@ -220,6 +220,31 @@ namespace osu.Game.Tests.Chat
AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty);
}
[Test]
public void TestPrivateChannelsPurgedOnUserChange()
{
var pmChannel = createChannel(1002, ChannelType.PM);
AddStep("join a few private channels", () =>
{
channelManager.JoinChannel(createChannel(1001, ChannelType.PM));
channelManager.JoinChannel(createChannel(1003, ChannelType.Team));
channelManager.JoinChannel(pmChannel);
});
AddStep("close a PM channel", () => channelManager.LeaveChannel(pmChannel));
AddStep("switch user", () =>
{
((DummyAPIAccess.DummyLocalUserState)API.LocalUserState).User.Value = new APIUser
{
Id = 9009,
Username = "someone_else"
};
});
AddAssert("not joined to private channels of previous user",
() => !channelManager.JoinedChannels.Select(ch => ch.Id).Any(id => id >= 1001 && id <= 1003));
}
private void handlePostMessageRequest(PostMessageRequest request)
{
var message = new Message(++currentMessageId)
@@ -250,7 +275,7 @@ namespace osu.Game.Tests.Chat
}
}
private Channel createChannel(int id, ChannelType type) => new Channel(new APIUser())
private Channel createChannel(int id, ChannelType type) => new Channel(new APIUser { Id = id })
{
Id = id,
Name = $"Channel {id}",
@@ -738,6 +738,16 @@ namespace osu.Game.Tests.NonVisual.Filtering
new object[] { "submitted=99999", false },
new object[] { "submitted>=2012-03-05-04", false },
new object[] { "submitted>=2012/03.05-04", false },
new object[] { "created<2012", true },
new object[] { "created<2012.03", true },
new object[] { "created<2012/03/05", true },
new object[] { "created<2012-3-5", true },
new object[] { "created<0", false },
new object[] { "created=99999", false },
new object[] { "created>=2012-03-05-04", false },
new object[] { "created>=2012/03.05-04", false },
};
[Test]
@@ -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 NUnit.Framework;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
@@ -149,5 +150,33 @@ namespace osu.Game.Tests.Online.Matchmaking
Assert.AreEqual(5, state.Users.GetOrAdd(5).Placement);
Assert.AreEqual(6, state.Users.GetOrAdd(6).Placement);
}
[Test]
public void AbandonOrder()
{
var state = new MatchmakingRoomState();
state.AdvanceRound();
state.RecordScores(
[
new SoloScoreInfo { UserID = 1, TotalScore = 1000 },
new SoloScoreInfo { UserID = 2, TotalScore = 500 },
], placement_points);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
state.Users.GetOrAdd(1).AbandonedAt = DateTimeOffset.Now;
state.RecordScores([], placement_points);
Assert.AreEqual(2, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(1, state.Users.GetOrAdd(2).Placement);
state.Users.GetOrAdd(2).AbandonedAt = DateTimeOffset.Now - TimeSpan.FromMinutes(1);
state.RecordScores([], placement_points);
Assert.AreEqual(1, state.Users.GetOrAdd(1).Placement);
Assert.AreEqual(2, state.Users.GetOrAdd(2).Placement);
}
}
}
@@ -266,7 +266,11 @@ namespace osu.Game.Tests.Visual.Background
FadeAccessibleResults results = null;
AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(TestResources.CreateTestScoreInfo())));
AddStep("Transition to Results", () =>
{
player.ValidForResume = false;
player.Push(results = new FadeAccessibleResults(TestResources.CreateTestScoreInfo()));
});
AddUntilStep("Wait for results is current", () => results.IsCurrentScreen());
@@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
double lastStarRating = 0;
double lastLength = 0;
AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(200, new TimingControlPoint { BeatLength = 600 }));
AddStep("Add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 600 }));
AddStep("Change to placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
@@ -7,6 +7,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
@@ -27,7 +28,12 @@ namespace osu.Game.Tests.Visual.Editing
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new TestBeatmap(ruleset, false);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
return beatmap;
}
private GlobalActionContainer globalActionContainer => this.ChildrenOfType<GlobalActionContainer>().Single();
@@ -10,6 +10,7 @@ using NUnit.Framework;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
@@ -27,7 +28,12 @@ namespace osu.Game.Tests.Visual.Editing
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new TestBeatmap(ruleset, false);
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
return beatmap;
}
private TimelineBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<TimelineBlueprintContainer>().First();
@@ -80,7 +86,7 @@ namespace osu.Game.Tests.Visual.Editing
{
InputManager.Key(Key.Number1);
blueprint = this.ChildrenOfType<TimelineHitObjectBlueprint>().First();
InputManager.MoveMouseTo(blueprint);
InputManager.MoveMouseTo(blueprint, new Vector2(-1, 0));
InputManager.Click(MouseButton.Left);
});
@@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Select.Leaderboards;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneDrawableGameplayLeaderboardScore : OsuTestScene
{
private readonly APIUser user = new APIUser { Username = "user" };
private readonly BindableLong totalScore = new BindableLong();
private readonly Bindable<int?> position = new Bindable<int?>();
private readonly BindableBool quit = new BindableBool();
private readonly BindableBool expanded = new BindableBool();
public TestSceneDrawableGameplayLeaderboardScore()
{
AddSliderStep("total score", 0, 1_000_000, 500_000, s => totalScore.Value = s);
AddSliderStep("position", 1, 100, 5, s => position.Value = s);
AddToggleStep("toggle quit", q => quit.Value = q);
AddToggleStep("toggle expanded", e => expanded.Value = e);
}
private static readonly OsuColour osu_colour = new OsuColour();
private static readonly object?[][] leaderboard_variants =
{
new object?[] { false, null },
new object?[] { true, null },
new object?[] { false, osu_colour.TeamColourRed },
new object?[] { true, osu_colour.TeamColourRed },
new object?[] { false, osu_colour.TeamColourBlue },
new object?[] { true, osu_colour.TeamColourBlue },
};
[TestCaseSource(nameof(leaderboard_variants))]
public void TestVariants(bool tracked, Color4? teamColour)
{
AddStep("show", () =>
{
GameplayLeaderboardScore score = new GameplayLeaderboardScore(user, tracked, totalScore)
{
Position = { BindTarget = position },
HasQuit = { BindTarget = quit },
TeamColour = teamColour,
};
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = 250,
Child = new DrawableGameplayLeaderboardScore(score)
{
Expanded = { BindTarget = expanded },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
});
}
}
}
@@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.Matchmaking
{
public abstract partial class MatchmakingTestScene : MultiplayerTestScene
{
protected override Container<Drawable> Content { get; }
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
protected MatchmakingTestScene()
{
BackgroundScreenStack backgroundStack;
base.Content.AddRange(new Drawable[]
{
backgroundStack = new BackgroundScreenStack(),
Content = new Container { RelativeSizeAxes = Axes.Both }
});
backgroundStack.Push(new MatchmakingBackgroundScreen(colourProvider));
}
}
}
@@ -8,7 +8,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@@ -17,12 +16,11 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene
public partial class TestSceneBeatmapSelectGrid : MatchmakingTestScene
{
private MatchmakingPlaylistItem[] items = null!;
@@ -131,7 +129,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
var (candidateItems, finalItem) = pickRandomItems(5);
grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem);
grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, finalItem);
});
}
@@ -160,7 +158,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
grid.PlayRollAnimation(finalItem, duration: 0);
Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem), 500);
Scheduler.AddDelayed(() => grid.PresentRolledBeatmap(finalItem, finalItem), 500);
});
}
@@ -175,7 +173,25 @@ namespace osu.Game.Tests.Visual.Matchmaking
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
grid.PlayRollAnimation(finalItem, duration: 0);
Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem), 500);
Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(finalItem, finalItem), 500);
});
}
[Test]
public void TestPresentRandomItem()
{
AddStep("present random item panel", () =>
{
var (candidateItems, finalItem) = pickRandomItems(4);
grid.TransferCandidatePanelsToRollContainer(candidateItems.Append(-1).ToArray(), duration: 0);
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
grid.PlayRollAnimation(-1, duration: 0);
Scheduler.AddDelayed(() =>
{
grid.PresentRolledBeatmap(-1, finalItem);
}, 500);
});
}
@@ -220,20 +236,19 @@ namespace osu.Game.Tests.Visual.Matchmaking
}
[Test]
public void TestPresentRandomItem()
public void TestRollAnimationFinalRandom()
{
AddStep("present random item panel", () =>
AddStep("play animation", () =>
{
grid.TransferCandidatePanelsToRollContainer(pickRandomItems(4).candidateItems.Append(-1).ToArray(), duration: 0);
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
grid.PlayRollAnimation(-1, duration: 0);
(long[] candidateItems, _) = pickRandomItems(5);
Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(-1), 500);
candidateItems = candidateItems.Append(-1).ToArray();
long finalItem = items.First(i => !candidateItems.Contains(i.ID)).ID;
grid.RollAndDisplayFinalBeatmap(candidateItems, -1, finalItem);
});
AddWaitStep("wait for animation", 5);
AddStep("reveal beatmap", () => grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem));
AddWaitStep("wait for animation", 10);
}
private (long[] candidateItems, long finalItem) pickRandomItems(int count)
@@ -12,11 +12,10 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneBeatmapSelectPanel : MultiplayerTestScene
public partial class TestSceneBeatmapSelectPanel : MatchmakingTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
@@ -99,7 +98,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddToggleStep("allow selection", value => panel!.AllowSelection = value);
AddStep("reveal beatmap", () => panel!.RevealBeatmap(CreateAPIBeatmap(), []));
AddStep("reveal beatmap", () => panel!.PresentAsChosenBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [])));
}
[Test]
@@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneMatchmakingChatDisplay : ScreenTestScene
public partial class TestSceneMatchmakingChatDisplay : MatchmakingTestScene
{
private MatchmakingChatDisplay? chat;
@@ -22,11 +22,11 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
Value =
[
new MatchmakingPool { Id = 0, RulesetId = 0, Name = "osu!" },
new MatchmakingPool { Id = 1, RulesetId = 1, Name = "osu!taiko" },
new MatchmakingPool { Id = 2, RulesetId = 2, Name = "osu!catch" },
new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4, Name = "osu!mania (4k)" },
new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7, Name = "osu!mania (7k)" },
new MatchmakingPool { Id = 0, RulesetId = 0, Name = "Free-for-all" },
new MatchmakingPool { Id = 1, RulesetId = 1, Name = "1v1" },
new MatchmakingPool { Id = 2, RulesetId = 2, Name = "1v1" },
new MatchmakingPool { Id = 3, RulesetId = 3, Variant = 4, Name = "1v1" },
new MatchmakingPool { Id = 4, RulesetId = 3, Variant = 7, Name = "1v1" },
]
}
});
@@ -110,6 +110,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
state.CandidateItems = beatmaps.Select(b => b.ID).ToArray();
state.CandidateItem = beatmaps[0].ID;
state.GameplayItem = beatmaps[0].ID;
}, waitTime: 35);
changeStage(MatchmakingStage.WaitingForClientsBeatmapDownload);
@@ -3,11 +3,10 @@
using osu.Framework.Graphics;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestScenePanelRoomAward : MultiplayerTestScene
public partial class TestScenePanelRoomAward : MatchmakingTestScene
{
public override void SetUpSteps()
{
@@ -6,16 +6,16 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestScenePickScreen : MultiplayerTestScene
public partial class TestScenePickScreen : MatchmakingTestScene
{
private readonly IReadOnlyList<APIUser> users = new[]
{
@@ -104,8 +104,28 @@ namespace osu.Game.Tests.Visual.Matchmaking
long[] candidateItems = selectedItems.ToArray();
long finalItem = candidateItems[Random.Shared.Next(candidateItems.Length)];
screen.RollFinalBeatmap(candidateItems, finalItem);
screen.RollFinalBeatmap(candidateItems, finalItem, finalItem);
});
}
[Test]
public void TestExpiredBeatmapNotShown()
{
SubScreenBeatmapSelect screen = null!;
AddStep("add screen with expired items", () =>
{
MultiplayerClient.ClientRoom!.Playlist =
[
new MultiplayerPlaylistItem(items[0]) { Expired = true },
new MultiplayerPlaylistItem(items[1])
];
Child = new ScreenStack(screen = new SubScreenBeatmapSelect());
});
AddUntilStep("items displayed", () => screen.ChildrenOfType<MatchmakingSelectPanelBeatmap>().Any());
AddAssert("expired item not shown", () => screen.ChildrenOfType<MatchmakingSelectPanelBeatmap>().Count(), () => Is.EqualTo(1));
}
}
}
@@ -11,12 +11,11 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestScenePlayerPanel : MultiplayerTestScene
public partial class TestScenePlayerPanel : MatchmakingTestScene
{
private PlayerPanel panel = null!;
@@ -15,12 +15,11 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Tests.Visual.Multiplayer;
using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestScenePlayerPanelOverlay : MultiplayerTestScene
public partial class TestScenePlayerPanelOverlay : MatchmakingTestScene
{
private PlayerPanelOverlay list = null!;
@@ -11,12 +11,11 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results;
using osu.Game.Tests.Visual.Multiplayer;
using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneResultsScreen : MultiplayerTestScene
public partial class TestSceneResultsScreen : MatchmakingTestScene
{
public override void SetUpSteps()
{
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Utils;
@@ -14,12 +15,11 @@ using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundResults;
using osu.Game.Tests.Visual.Multiplayer;
using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneRoundResultsScreen : MultiplayerTestScene
public partial class TestSceneRoundResultsScreen : MatchmakingTestScene
{
public override void SetUpSteps()
{
@@ -27,8 +27,15 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined();
}
setupRequestHandler();
[TestCase(2)]
[TestCase(4)]
[TestCase(8)]
[TestCase(16)]
public void TestDisplayScores(int scoreCount)
{
setupRequestHandler(scoreCount);
AddStep("load screen", () =>
{
@@ -41,7 +48,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
});
}
private void setupRequestHandler()
private void setupRequestHandler(int scoreCount)
{
AddStep("setup request handler", () =>
{
@@ -72,7 +79,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
case IndexPlaylistScoresRequest index:
var result = new IndexedMultiplayerScores();
for (int i = 0; i < 8; ++i)
for (int i = 0; i < scoreCount; ++i)
{
result.Scores.Add(new MultiplayerScore
{
@@ -9,11 +9,10 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneStageDisplay : MultiplayerTestScene
public partial class TestSceneStageDisplay : MatchmakingTestScene
{
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
@@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
@@ -33,6 +35,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
Children = new Drawable[]
{
new MultiplayerSkipOverlay(120000)
{
RequestSkip = () => MultiplayerClient.VoteToSkipIntro().WaitSafely(),
}
},
};
@@ -47,26 +52,83 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
for (int i = 0; i < 4; i++)
{
int i2 = i;
int userId = i;
AddStep($"join user {i2}", () =>
AddStep($"join user {userId}", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = i2,
Username = $"User {i2}"
Id = userId,
Username = $"User {userId}"
});
MultiplayerClient.ChangeUserState(i2, MultiplayerUserState.Playing);
MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing);
});
}
AddStep("local user votes", () => MultiplayerClient.VoteToSkipIntro().WaitSafely());
AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely());
AddStep("local user votes", () => this.ChildrenOfType<MultiplayerSkipOverlay.Button>().Single().TriggerClick());
AddStep("user 1 votes", () => MultiplayerClient.UserVoteToSkipIntro(1).WaitSafely());
}
[Test]
public void TestLeavingBeforeLocalVote()
{
for (int i = 0; i < 4; i++)
{
int i2 = i;
AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkipIntro(i2).WaitSafely());
int userId = i;
AddStep($"join user {userId}", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = userId,
Username = $"User {userId}"
});
MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing);
});
}
AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely());
AddStep("user 1 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 }));
AddStep("user 2 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 2 }));
AddStep("user 3 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 3 }));
AddStep("user 0 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 0 }));
}
[Test]
public void TestLeavingAfterLocalVote()
{
for (int i = 0; i < 4; i++)
{
int userId = i;
AddStep($"join user {userId}", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = userId,
Username = $"User {userId}"
});
MultiplayerClient.ChangeUserState(userId, MultiplayerUserState.Playing);
});
}
AddStep("local user votes", () => this.ChildrenOfType<MultiplayerSkipOverlay.Button>().Single().TriggerClick());
AddStep("user 0 votes", () => MultiplayerClient.UserVoteToSkipIntro(0).WaitSafely());
AddStep("user 1 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 }));
AddStep("user 2 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 2 }));
AddStep("user 3 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 3 }));
AddStep("user 0 leaves", () => MultiplayerClient.RemoveUser(new APIUser { Id = 0 }));
}
public partial class TestMultiplayerSkipOverlay : MultiplayerSkipOverlay
{
public TestMultiplayerSkipOverlay()
: base(120000)
{
}
}
}
@@ -146,5 +146,165 @@ namespace osu.Game.Tests.Visual.Online
checkCount++;
}, 10);
}
[Test]
public void TestAlternatingBackgroundDoesNotChangeAtMaxHistory()
{
AddStep("fill up the channel", () =>
{
for (int i = 0; i < Channel.MAX_HISTORY; i++)
{
channel.AddNewMessages(new Message
{
ChannelId = channel.Id,
Content = $"Message {i}",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 3,
Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N")
}
});
}
});
AddUntilStep($"{Channel.MAX_HISTORY} messages present", () => drawableChannel.ChildrenOfType<ChatLine>().Count(), () => Is.EqualTo(Channel.MAX_HISTORY));
ChatLine? lastLine = null;
bool lastLineAlternatingBackground = false;
AddStep("grab last line", () =>
{
lastLine = drawableChannel.ChildrenOfType<ChatLine>().Last();
lastLineAlternatingBackground = lastLine.AlternatingBackground;
});
AddStep("add another message", () => channel.AddNewMessages(new Message
{
ChannelId = channel.Id,
Content = "One final message",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 3,
Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N")
}
}));
AddAssert("second-last message has same background", () => lastLine!.AlternatingBackground, () => Is.EqualTo(lastLineAlternatingBackground));
}
[Test]
public void TestAlternatingBackgroundUpdatedOnRemoval()
{
AddStep("add 3 messages", () =>
{
for (int i = 0; i < 3; i++)
{
channel.AddNewMessages(new Message
{
ChannelId = channel.Id,
Content = $"Message {i}",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = i,
Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N")
}
});
}
});
AddUntilStep("3 messages present", () => drawableChannel.ChildrenOfType<ChatLine>().Count(), () => Is.EqualTo(3));
assertAlternatingBackground(0, false);
assertAlternatingBackground(1, true);
assertAlternatingBackground(2, false);
AddStep("remove middle message", () => channel.RemoveMessagesFromUser(1));
AddUntilStep("2 messages present", () => drawableChannel.ChildrenOfType<ChatLine>().Count(), () => Is.EqualTo(2));
assertAlternatingBackground(0, true);
assertAlternatingBackground(1, false);
void assertAlternatingBackground(int lineIndex, bool shouldBeAlternating)
=> AddAssert($"line {lineIndex} {(shouldBeAlternating ? "has" : "does not have")} alternating background",
() => drawableChannel.ChildrenOfType<ChatLine>().ElementAt(lineIndex).AlternatingBackground,
() => Is.EqualTo(shouldBeAlternating));
}
[Test]
public void TestTimestampsUpdateOnRemoval()
{
AddStep("add 3 messages", () =>
{
channel.AddNewMessages(
new Message
{
ChannelId = channel.Id,
Content = "Message 0",
Timestamp = new DateTimeOffset(2022, 11, 21, 20, 0, 0, TimeSpan.Zero),
Sender = new APIUser
{
Id = 0,
Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N")
}
},
new Message
{
ChannelId = channel.Id,
Content = "Message 1",
Timestamp = new DateTimeOffset(2022, 11, 21, 20, 0, 0, TimeSpan.Zero).AddSeconds(1),
Sender = new APIUser
{
Id = 1,
Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N")
}
},
new Message
{
ChannelId = channel.Id,
Content = "Message 2",
Timestamp = new DateTimeOffset(2022, 11, 21, 20, 0, 0, TimeSpan.Zero).AddMinutes(1),
Sender = new APIUser
{
Id = 2,
Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N")
}
},
new Message
{
ChannelId = channel.Id,
Content = "Message 3",
Timestamp = new DateTimeOffset(2022, 11, 21, 20, 0, 0, TimeSpan.Zero).AddMinutes(1).AddSeconds(1),
Sender = new APIUser
{
Id = 3,
Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N")
}
}
);
});
AddUntilStep("4 messages present", () => drawableChannel.ChildrenOfType<ChatLine>().Count(), () => Is.EqualTo(4));
assertTimestamp(0, true);
assertTimestamp(1, false);
assertTimestamp(2, true);
assertTimestamp(3, false);
AddStep("remove message 0", () => channel.RemoveMessagesFromUser(0));
AddUntilStep("3 messages present", () => drawableChannel.ChildrenOfType<ChatLine>().Count(), () => Is.EqualTo(3));
assertTimestamp(0, true);
assertTimestamp(1, true);
assertTimestamp(2, false);
AddStep("remove message 2", () => channel.RemoveMessagesFromUser(2));
AddUntilStep("2 messages present", () => drawableChannel.ChildrenOfType<ChatLine>().Count(), () => Is.EqualTo(2));
assertTimestamp(0, true);
assertTimestamp(1, true);
void assertTimestamp(int lineIndex, bool shouldHaveTimestamp)
=> AddAssert($"line {lineIndex} {(shouldHaveTimestamp ? "has" : "does not have")} timestamp",
() => drawableChannel.ChildrenOfType<ChatLine>().ElementAt(lineIndex).RequiresTimestamp,
() => Is.EqualTo(shouldHaveTimestamp));
}
}
}
@@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Settings
scrollToAndStartBinding("Increase volume");
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
checkBinding("Increase volume", "LShift");
checkBinding("Increase volume", "Shift");
}
[Test]
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("press shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("press k", () => InputManager.Key(Key.K));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
checkBinding("Increase volume", "LShift-K");
checkBinding("Increase volume", "Shift-K");
}
[Test]
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("schedule button clicks", () =>
{
var clearButton = firstRow.ChildrenOfType<KeyBindingRow.ClearButton>().Single();
var clearButton = firstRow.ChildrenOfType<DangerousRoundedButton>().Single();
InputManager.MoveMouseTo(clearButton);
@@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Settings
{
AddStep("click clear button", () =>
{
var clearButton = multiBindingRow.ChildrenOfType<KeyBindingRow.ClearButton>().Single();
var clearButton = multiBindingRow.ChildrenOfType<DangerousRoundedButton>().Single();
InputManager.MoveMouseTo(clearButton);
InputManager.Click(MouseButton.Left);
@@ -386,7 +386,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("clear binding", () =>
{
var row = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text.ToString() == "Left (centre)"));
row.ChildrenOfType<KeyBindingRow.ClearButton>().Single().TriggerClick();
row.ChildrenOfType<DangerousRoundedButton>().Single().TriggerClick();
});
scrollToAndStartBinding("Left (rim)");
AddStep("bind M1", () => InputManager.Click(MouseButton.Left));
@@ -462,7 +462,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("clear binding", () =>
{
var row = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text.ToString() == "Left (centre)"));
row.ChildrenOfType<KeyBindingRow.ClearButton>().Single().TriggerClick();
row.ChildrenOfType<DangerousRoundedButton>().Single().TriggerClick();
});
}
@@ -22,7 +22,6 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
@@ -144,41 +143,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2
void onScreenPushed(IScreen lastScreen, IScreen newScreen) => screensPushed.Add(lastScreen);
}
[TestCase(true)]
[TestCase(false)]
public void TestHoveringLeftSideReexpandsGroupSelectionIsIn(bool mouseOverPanel)
{
ImportBeatmapForRuleset(0);
LoadSongSelect();
SortAndGroupBy(SortMode.Difficulty, GroupMode.Difficulty);
AddStep("move mouse to carousel", () => InputManager.MoveMouseTo(Carousel));
AddUntilStep("expanded group is below 1 star",
() => (Carousel.ChildrenOfType<PanelGroupStarDifficulty>().SingleOrDefault(p => p.Expanded.Value)?.Item?.Model as StarDifficultyGroupDefinition)?.Difficulty.Stars,
() => Is.EqualTo(0));
AddStep("select next group", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Right);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddUntilStep("expanded group is 3 star",
() => (Carousel.ChildrenOfType<PanelGroupStarDifficulty>().SingleOrDefault(p => p.Expanded.Value)?.Item?.Model as StarDifficultyGroupDefinition)?.Difficulty.Stars,
() => Is.EqualTo(3));
if (mouseOverPanel)
AddStep("move mouse over left panel", () => InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapTitleWedge>().Single()));
else
AddStep("move mouse to left side container", () => InputManager.MoveMouseTo(this.ChildrenOfType<Screens.Select.SongSelect.LeftSideInteractionContainer>().Single()));
AddUntilStep("expanded group is below 1 star",
() => (Carousel.ChildrenOfType<PanelGroupStarDifficulty>().Single(p => p.Expanded.Value).Item?.Model as StarDifficultyGroupDefinition)?.Difficulty.Stars,
() => Is.EqualTo(0));
}
#region Hotkeys
[Test]
@@ -4,12 +4,12 @@
#nullable disable
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings.Sections;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
@@ -19,9 +19,12 @@ namespace osu.Game.Tests.Visual.UserInterface
private TestExpandingContainer container;
private SettingsToolboxGroup toolboxGroup;
private ExpandableSlider<float, SizeSlider<float>> slider1;
private ExpandableSlider<float> slider1;
private ExpandableSlider<double> slider2;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[SetUp]
public void SetUp() => Schedule(() =>
{
@@ -36,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Width = 1,
Children = new Drawable[]
{
slider1 = new ExpandableSlider<float, SizeSlider<float>>
slider1 = new ExpandableSlider<float>
{
Current = new BindableFloat
{
@@ -62,13 +65,13 @@ namespace osu.Game.Tests.Visual.UserInterface
slider1.Current.BindValueChanged(v =>
{
slider1.ExpandedLabelText = $"Slider One ({v.NewValue:0.##x})";
slider1.ExpandedLabelText = "Slider One";
slider1.ContractedLabelText = $"S. 1. ({v.NewValue:0.##x})";
}, true);
slider2.Current.BindValueChanged(v =>
{
slider2.ExpandedLabelText = $"Slider Two ({v.NewValue:N2})";
slider2.ExpandedLabelText = "Slider Two";
slider2.ContractedLabelText = $"S. 2. ({v.NewValue:N2})";
}, true);
});
@@ -1,20 +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 System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneFormSliderBar : OsuTestScene
public partial class TestSceneFormSliderBar : OsuManualInputManagerTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
@@ -59,5 +63,49 @@ namespace osu.Game.Tests.Visual.UserInterface
slider.TransferValueOnCommit = b;
});
}
[Test]
public void TestNubDoubleClickRevertToDefault()
{
FormSliderBar<float> slider = null!;
AddStep("create content", () =>
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
slider = new FormSliderBar<float>
{
Caption = "Slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
Default = 5f,
}
},
}
};
});
AddStep("set slider to 1", () => slider.Current.Value = 1);
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType<Circle>().Single()));
AddStep("double click nub", () =>
{
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddAssert("slider is default", () => slider.Current.IsDefault);
}
}
}
@@ -5,6 +5,10 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models;
@@ -52,6 +56,7 @@ namespace osu.Game.Tournament.Tests.Components
beatmap.ApproachRate = 6.8f;
beatmap.OverallDifficulty = 5.5f;
beatmap.StarRating = 4.56f;
beatmap.DrainRate = 1.23f;
beatmap.Length = 123456;
beatmap.BPM = 133;
beatmap.OnlineID = ladderBeatmap.OnlineID;
@@ -61,11 +66,18 @@ namespace osu.Game.Tournament.Tests.Components
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
AddStep("set mods to HDHRDT", () => songBar.Mods = LegacyMods.Hidden | LegacyMods.HardRock | LegacyMods.DoubleTime);
AddStep("unset mods", () => songBar.Mods = LegacyMods.None);
AddToggleStep("toggle expanded", expanded => songBar.Expanded = expanded);
AddStep("set null beatmap", () => songBar.Beatmap = null);
AddStep("set ruleset to osu", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
AddStep("set ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
AddStep("set ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
AddStep("set ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
}
}
}
@@ -6,6 +6,8 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat;
@@ -61,14 +63,33 @@ namespace osu.Game.Tournament.Tests.Components
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
chatDisplay.Channel.Value = testChannel;
}
protected override void LoadComplete()
{
base.LoadComplete();
AddStep("set up API", () =>
{
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case JoinChannelRequest joinChannelRequest:
joinChannelRequest.TriggerSuccess();
return true;
case LeaveChannelRequest leaveChannelRequest:
leaveChannelRequest.TriggerSuccess();
return true;
default:
return false;
}
};
});
AddStep("set channel", () => chatDisplay.Channel.Value = testChannel);
AddStep("message from admin", () => testChannel.AddNewMessages(new Message(nextMessageId())
{
Sender = admin,
+19 -25
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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -14,6 +15,7 @@ using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu;
using osu.Game.Utils;
using osuTK;
@@ -123,27 +125,19 @@ namespace osu.Game.Tournament.Components
},
};
double bpm = beatmap.BPM;
double length = beatmap.Length;
string hardRockExtra = "";
var rulesetInstance = ruleset.Value.CreateInstance();
var convertedMods = rulesetInstance.ConvertFromLegacyMods(mods).ToList();
var adjustedDifficulty = rulesetInstance.GetAdjustedDisplayDifficulty(beatmap, convertedMods);
double rate = ModUtils.CalculateRateWithMods(convertedMods);
double bpm = FormatUtils.RoundBPM(beatmap.BPM, rate);
double length = beatmap.Length / rate;
string srExtra = "";
float ar = beatmap.Difficulty.ApproachRate;
if ((mods & LegacyMods.HardRock) > 0)
if (convertedMods.Any(x => x is ModHardRock) || convertedMods.Any(x => x is ModDoubleTime))
{
hardRockExtra = "*";
srExtra = "*";
}
if ((mods & LegacyMods.DoubleTime) > 0)
{
// temporary local calculation (taken from OsuDifficultyCalculator)
double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(ar, 1800, 1200, 450) / 1.5;
ar = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5);
bpm *= 1.5f;
length /= 1.5f;
srExtra = "*";
}
@@ -154,9 +148,9 @@ namespace osu.Game.Tournament.Components
default:
stats = new (string heading, string content)[]
{
("CS", $"{beatmap.Difficulty.CircleSize:0.#}{hardRockExtra}"),
("AR", $"{ar:0.#}{hardRockExtra}"),
("OD", $"{beatmap.Difficulty.OverallDifficulty:0.#}{hardRockExtra}"),
("CS", $"{adjustedDifficulty.CircleSize:0.#}"),
("AR", $"{adjustedDifficulty.ApproachRate:0.#}"),
("OD", $"{adjustedDifficulty.OverallDifficulty:0.#}"),
};
break;
@@ -164,16 +158,16 @@ namespace osu.Game.Tournament.Components
case 3:
stats = new (string heading, string content)[]
{
("OD", $"{beatmap.Difficulty.OverallDifficulty:0.#}{hardRockExtra}"),
("HP", $"{beatmap.Difficulty.DrainRate:0.#}{hardRockExtra}")
("OD", $"{adjustedDifficulty.OverallDifficulty:0.#}"),
("HP", $"{adjustedDifficulty.DrainRate:0.#}")
};
break;
case 2:
stats = new (string heading, string content)[]
{
("CS", $"{beatmap.Difficulty.CircleSize:0.#}{hardRockExtra}"),
("AR", $"{ar:0.#}"),
("CS", $"{adjustedDifficulty.CircleSize:0.#}"),
("AR", $"{adjustedDifficulty.ApproachRate:0.#}"),
};
break;
}
@@ -16,7 +16,7 @@ namespace osu.Game.Tournament.Components
{
public partial class TournamentMatchChatDisplay : StandAloneChatDisplay
{
private readonly Bindable<string> chatChannel = new Bindable<string>();
private readonly Bindable<string> channelName = new Bindable<string>();
private ChannelManager? manager;
@@ -34,39 +34,33 @@ namespace osu.Game.Tournament.Components
}
[BackgroundDependencyLoader]
private void load(MatchIPCInfo? ipc, IAPIProvider api)
private void load(MatchIPCInfo ipc, IAPIProvider api)
{
if (ipc != null)
AddInternal(manager = new ChannelManager(api));
Channel.BindTo(manager.CurrentChannel);
channelName.BindTo(ipc.ChatChannel);
channelName.BindValueChanged(c =>
{
chatChannel.BindTo(ipc.ChatChannel);
chatChannel.BindValueChanged(c =>
if (int.TryParse(c.OldValue, out int oldChannelId) && oldChannelId > 0)
{
if (string.IsNullOrWhiteSpace(c.NewValue))
return;
int id = int.Parse(c.NewValue);
if (id <= 0) return;
if (manager == null)
{
AddInternal(manager = new ChannelManager(api));
Channel.BindTo(manager.CurrentChannel);
}
foreach (var ch in manager.JoinedChannels.ToList())
manager.LeaveChannel(ch);
var joinedChannel = manager.JoinedChannels.SingleOrDefault(ch => ch.Id == oldChannelId);
if (joinedChannel != null)
manager.LeaveChannel(joinedChannel);
}
if (int.TryParse(c.NewValue, out int newChannelId) && newChannelId > 0)
{
var channel = new Channel
{
Id = id,
Id = newChannelId,
Type = ChannelType.Public
};
manager.JoinChannel(channel);
manager.CurrentChannel.Value = channel;
}, true);
}
}
}, true);
}
public void Expand() => this.FadeIn(300);
@@ -6,6 +6,7 @@ using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using static osu.Game.Online.API.Requests.Responses.APIBeatmap;
namespace osu.Game.Tournament.Models
{
@@ -31,6 +32,8 @@ namespace osu.Game.Tournament.Models
public BeatmapSetOnlineCovers Covers { get; set; }
public IRulesetInfo Ruleset { get; set; } = new APIRuleset();
public TournamentBeatmap()
{
}
@@ -47,6 +50,7 @@ namespace osu.Game.Tournament.Models
Covers = beatmap.BeatmapSet?.Covers ?? new BeatmapSetOnlineCovers();
EndTimeObjectCount = beatmap.EndTimeObjectCount;
TotalObjectCount = beatmap.TotalObjectCount;
Ruleset = beatmap.Ruleset;
}
public bool Equals(IBeatmapInfo? other) => other is TournamentBeatmap b && this.MatchesOnlineID(b);
@@ -83,7 +87,7 @@ namespace osu.Game.Tournament.Models
string IBeatmapInfo.MD5Hash => throw new NotImplementedException();
IRulesetInfo IBeatmapInfo.Ruleset => throw new NotImplementedException();
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
DateTimeOffset IBeatmapSetOnlineInfo.Submitted => throw new NotImplementedException();
+7 -1
View File
@@ -105,6 +105,7 @@ namespace osu.Game.Beatmaps
return (beatLength: t.BeatLength, duration: nextTime - currentTime);
})
// Aggregate durations into a set of (beatLength, duration) tuples for each beat length
// Rounding is applied here (to 1e-3 milliseconds) to neutralise potential effects of floating point inaccuracies
.GroupBy(t => Math.Round(t.beatLength * 1000) / 1000)
.Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration)))
// Get the most common one, or 0 as a suitable default (see handling below)
@@ -113,7 +114,12 @@ namespace osu.Game.Beatmaps
if (mostCommon.beatLength == 0)
return TimingControlPoint.DEFAULT_BEAT_LENGTH;
return mostCommon.beatLength;
// Because of the rounding applied to the beat length above, it is possible for the "most common" beat length as determined by the linq query above
// to actually be less or more than the raw range of unrounded beat lengths present in the map
// To ensure this does not become a problem anywhere else further, clamp the result to the known raw range
double minBeatLength = ControlPointInfo.TimingPoints.Min(t => t.BeatLength);
double maxBeatLength = ControlPointInfo.TimingPoints.Max(t => t.BeatLength);
return Math.Clamp(mostCommon.beatLength, minBeatLength, maxBeatLength);
}
public double AudioLeadIn { get; set; }
@@ -192,6 +192,8 @@ namespace osu.Game.Beatmaps.Drawables
"2412260 Koto Spirit - Locus of Hexagram.osz",
"2412232 Will Stetson - Of Our Time.osz",
"2412292 ArXe - Locus Amoenus (feat. Megurine Luka).osz",
"2412328 Akiri - Vespera Stella.osz",
"2412331 takehirotei - Haiboku no Altra Vita.osz",
};
private static readonly string[] bundled_osu =
@@ -4,7 +4,7 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Input;
using osu.Framework.Threading;
namespace osu.Game.Graphics.Containers
@@ -58,6 +58,19 @@ namespace osu.Game.Graphics.Containers
protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer();
private InputManager inputManager = null!;
/// <summary>
/// Tracks whether the mouse was in bounds of this expanding container in the last frame.
/// </summary>
private bool? lastMouseInBounds;
/// <summary>
/// Tracks whether the last expansion of the container was caused by the mouse moving into its bounds
/// (as opposed to an external set of `Expanded`, in which case moving the mouse outside of its bounds should not contract).
/// </summary>
private bool? expandedByMouse;
private ScheduledDelegate? hoverExpandEvent;
protected override void LoadComplete()
@@ -68,37 +81,43 @@ namespace osu.Game.Graphics.Containers
{
this.ResizeWidthTo(v.NewValue ? expandedWidth : contractedWidth, TRANSITION_DURATION, Easing.OutQuint);
}, true);
inputManager = GetContainingInputManager()!;
}
protected override bool OnHover(HoverEvent e)
protected override void Update()
{
updateHoverExpansion();
return true;
base.Update();
bool mouseInBounds = Contains(inputManager.CurrentState.Mouse.Position);
if (lastMouseInBounds != mouseInBounds)
updateExpansionState(mouseInBounds);
lastMouseInBounds = mouseInBounds;
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (hoverExpandEvent != null)
{
hoverExpandEvent?.Cancel();
hoverExpandEvent = null;
Expanded.Value = false;
return;
}
base.OnHoverLost(e);
}
private void updateHoverExpansion()
private void updateExpansionState(bool mouseInBounds)
{
if (!ExpandOnHover)
return;
hoverExpandEvent?.Cancel();
hoverExpandEvent = null;
if (IsHovered && !Expanded.Value)
if (mouseInBounds && !Expanded.Value)
{
hoverExpandEvent = Scheduler.AddDelayed(() => Expanded.Value = true, HoverExpansionDelay);
expandedByMouse = true;
}
if (!mouseInBounds && Expanded.Value)
{
if (expandedByMouse == true)
Expanded.Value = false;
expandedByMouse = false;
}
}
}
}
@@ -6,7 +6,7 @@ using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Graphics.UserInterface
{
public partial class DangerousRoundedButton : RoundedButton
public sealed partial class DangerousRoundedButton : RoundedButton
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterface
@@ -19,49 +20,27 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public partial class ExpandableSlider<T, TSlider> : CompositeDrawable, IExpandable, IHasCurrentValue<T>
where T : struct, INumber<T>, IMinMaxValue<T>
where TSlider : RoundedSliderBar<T>, new()
where TSlider : FormSliderBar<T>, new()
{
private readonly OsuSpriteText label;
private readonly OsuSpriteText contractedLabel;
private readonly TSlider slider;
private LocalisableString contractedLabelText;
/// <summary>
/// The label text to display when this slider is in a contracted state.
/// </summary>
public LocalisableString ContractedLabelText
{
get => contractedLabelText;
set
{
if (value == contractedLabelText)
return;
contractedLabelText = value;
if (!Expanded.Value)
label.Text = value;
}
get => contractedLabel.Text;
set => contractedLabel.Text = value;
}
private LocalisableString expandedLabelText;
/// <summary>
/// The label text to display when this slider is in an expanded state.
/// </summary>
public LocalisableString ExpandedLabelText
{
get => expandedLabelText;
set
{
if (value == expandedLabelText)
return;
expandedLabelText = value;
if (Expanded.Value)
label.Text = value;
}
get => slider.Caption;
set => slider.Caption = value;
}
public Bindable<T> Current
@@ -95,7 +74,7 @@ namespace osu.Game.Graphics.UserInterface
Spacing = new Vector2(0f, 10f),
Children = new Drawable[]
{
label = new OsuSpriteText(),
contractedLabel = new OsuSpriteText(),
slider = new TSlider
{
RelativeSizeAxes = Axes.X,
@@ -118,7 +97,8 @@ namespace osu.Game.Graphics.UserInterface
Expanded.BindValueChanged(v =>
{
label.Text = v.NewValue ? expandedLabelText : contractedLabelText;
contractedLabel.FadeTo(v.NewValue ? 0 : 1);
slider.FadeTo(v.NewValue ? Current.Disabled ? 0.3f : 1f : 0f, 500, Easing.OutQuint);
slider.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
}, true);
@@ -133,7 +113,7 @@ namespace osu.Game.Graphics.UserInterface
/// <summary>
/// An <see cref="IExpandable"/> implementation for the UI slider bar control.
/// </summary>
public partial class ExpandableSlider<T> : ExpandableSlider<T, RoundedSliderBar<T>>
public partial class ExpandableSlider<T> : ExpandableSlider<T, FormSliderBar<T>>
where T : struct, INumber<T>, IMinMaxValue<T>
{
}
@@ -56,7 +56,8 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
CornerRadius = 5,
CornerRadius = 10,
CornerExponent = 2.5f,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
@@ -357,7 +357,8 @@ namespace osu.Game.Graphics.UserInterface
Icon = FontAwesome.Solid.ChevronDown,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(16),
Size = new Vector2(10),
Margin = new MarginPadding { Right = 2 },
},
}
}
@@ -16,6 +16,7 @@ namespace osu.Game.Graphics.UserInterface
public bool Seeking { get; private set; }
public Action<double> OnSeek;
public Action<double> OnCommit;
private readonly Box fill;
private readonly Box background;
@@ -80,12 +81,14 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnUserChange(double value)
{
Seeking = true;
OnSeek?.Invoke(value);
base.OnUserChange(value);
}
protected override bool Commit()
{
OnSeek?.Invoke(CurrentNumber.Value);
Seeking = false;
OnCommit?.Invoke(CurrentNumber.Value);
return base.Commit();
}
}
@@ -178,7 +178,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
Size = new Vector2(70);
Masking = true;
CornerRadius = 35;
CornerRadius = 10;
CornerExponent = 2.5f;
Action = this.ShowPopover;
Children = new Drawable[]
@@ -58,26 +58,49 @@ namespace osu.Game.Graphics.UserInterfaceV2
}
}
private LocalisableString caption;
/// <summary>
/// Caption describing this slider bar, displayed on top of the controls.
/// </summary>
public LocalisableString Caption { get; init; }
public LocalisableString Caption
{
get => caption;
set
{
caption = value;
if (IsLoaded)
captionText.Caption = value;
}
}
/// <summary>
/// Hint text containing an extended description of this slider bar, displayed in a tooltip when hovering the caption.
/// </summary>
public LocalisableString HintText { get; init; }
private float keyboardStep;
/// <summary>
/// A custom step value for each key press which actuates a change on this control.
/// </summary>
public float KeyboardStep { get; init; }
public float KeyboardStep
{
get => keyboardStep;
set
{
keyboardStep = value;
if (IsLoaded)
slider.KeyboardStep = value;
}
}
private Box background = null!;
private Box flashLayer = null!;
private FormTextBox.InnerTextBox textBox = null!;
private InnerSlider slider = null!;
private FormFieldCaption caption = null!;
private FormFieldCaption captionText = null!;
private IFocusManager focusManager = null!;
[Resolved]
@@ -117,11 +140,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
Children = new Drawable[]
{
caption = new FormFieldCaption
captionText = new FormFieldCaption
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Caption = Caption,
TooltipText = HintText,
},
textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true)
@@ -145,7 +167,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
KeyboardStep = KeyboardStep,
Current = currentNumberInstantaneous,
OnCommit = () => current.Value = currentNumberInstantaneous.Value,
}
@@ -161,6 +182,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
base.LoadComplete();
slider.KeyboardStep = keyboardStep;
captionText.Caption = caption;
focusManager = GetContainingFocusManager()!;
textBox.Focused.BindValueChanged(_ => updateState());
@@ -270,7 +294,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
textBox.Alpha = 1;
background.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background4 : colourProvider.Background5;
caption.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
captionText.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
textBox.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
BorderThickness = childHasFocus || IsHovered || slider.IsDragging.Value ? 2 : 0;
@@ -300,8 +324,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
private Box leftBox = null!;
private Box rightBox = null!;
private Circle nub = null!;
private const float nub_width = 10;
private InnerSliderNub nub = null!;
public const float NUB_WIDTH = 10;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -311,7 +335,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
Height = 40;
RelativeSizeAxes = Axes.X;
RangePadding = nub_width / 2;
RangePadding = NUB_WIDTH / 2;
Children = new Drawable[]
{
@@ -340,12 +364,13 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = RangePadding, },
Child = nub = new Circle
Child = nub = new InnerSliderNub
{
Width = nub_width,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
Origin = Anchor.TopCentre,
ResetToDefault = () =>
{
if (!Current.Disabled)
Current.SetDefault();
}
}
},
new HoverClickSounds()
@@ -428,5 +453,27 @@ namespace osu.Game.Graphics.UserInterfaceV2
return result;
}
}
private partial class InnerSliderNub : Circle
{
public Action? ResetToDefault { get; set; }
[BackgroundDependencyLoader]
private void load()
{
Width = InnerSlider.NUB_WIDTH;
RelativeSizeAxes = Axes.Y;
RelativePositionAxes = Axes.X;
Origin = Anchor.TopCentre;
}
protected override bool OnClick(ClickEvent e) => true; // must be handled for double click handler to ever fire
protected override bool OnDoubleClick(DoubleClickEvent e)
{
ResetToDefault?.Invoke();
return true;
}
}
}
}
@@ -26,18 +26,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
private Color4? triangleGradientSecondColour;
public override float Height
{
get => base.Height;
set
{
base.Height = value;
if (IsLoaded)
updateCornerRadius();
}
}
public override Color4 BackgroundColour
{
get => base.BackgroundColour;
@@ -61,7 +49,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
base.LoadComplete();
updateCornerRadius();
// This doesn't match the latest design spec (should be 5) but is an in-between that feels right to the eye
// until we move everything over to Form controls.
Content.CornerRadius = 10;
Content.CornerExponent = 2.5f;
Add(Triangles = new TrianglesV2
{
@@ -98,8 +89,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
base.OnHoverLost(e);
}
private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2;
public virtual IEnumerable<LocalisableString> FilterTerms => new[] { Text };
public bool MatchingFilter
+5
View File
@@ -199,6 +199,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Mapper => new TranslatableString(getKey(@"mapper"), @"Mapper");
/// <summary>
/// "Delete..."
/// </summary>
public static LocalisableString DeleteWithConfirmation => new TranslatableString(getKey(@"delete_with_confrmation"), @"Delete...");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -29,6 +29,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Prefer24HourTimeDisplay => new TranslatableString(getKey(@"prefer_24_hour_time_display"), @"Prefer 24-hour time display");
/// <summary>
/// "Installation"
/// </summary>
public static LocalisableString InstallationHeader => new TranslatableString(getKey(@"installation_header"), @"Installation");
/// <summary>
/// "Quick Actions"
/// </summary>
public static LocalisableString QuickActionsHeader => new TranslatableString(getKey(@"quick_actions_header"), @"Quick Actions");
/// <summary>
/// "Updates"
/// </summary>
@@ -79,6 +89,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString LearnMoreAboutLazerTooltip => new TranslatableString(getKey(@"check_out_the_feature_comparison"), @"Check out the feature comparison and FAQ");
/// <summary>
/// "Report an issue"
/// </summary>
public static LocalisableString ReportIssue => new TranslatableString(getKey(@"report_issue"), @"Report an issue");
/// <summary>
/// "Report a problem with the game to the developers."
/// </summary>
public static LocalisableString ReportIssueTooltip => new TranslatableString(getKey(@"report_issue_tooltip"), @"Report a problem with the game to the developers.");
/// <summary>
/// "Check with your package manager / provider for other release streams."
/// </summary>
+5 -5
View File
@@ -109,6 +109,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString UseTheseMods => new TranslatableString(getKey(@"use_these_mods"), @"Use these mods");
/// <summary>
/// "Watch replay"
/// </summary>
public static LocalisableString WatchReplay => new TranslatableString(getKey(@"watch_replay"), @"Watch replay");
/// <summary>
/// "For all difficulties"
/// </summary>
@@ -139,11 +144,6 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ClearAllLocalScores => new TranslatableString(getKey(@"clear_all_local_scores"), @"Clear all local scores");
/// <summary>
/// "Delete beatmap"
/// </summary>
public static LocalisableString DeleteBeatmap => new TranslatableString(getKey(@"delete_beatmap"), @"Delete beatmap");
/// <summary>
/// "Restore all hidden"
/// </summary>
+4
View File
@@ -62,6 +62,10 @@ namespace osu.Game.Online.API
localUser.Value = me;
configSupporter.Value = me.IsSupporter;
// `last_visit` is assumed to be `null` if and only if the web-side "hide online presence toggle" is enabled
if (me.LastVisit == null)
configStatus.Value = UserStatus.Offline;
UpdateFriends();
UpdateBlocks();
UpdateFavouriteBeatmapSets();
@@ -146,6 +146,7 @@ namespace osu.Game.Online.API.Requests.Responses
public string Name => $@"{nameof(APIRuleset)} (ID: {OnlineID})";
[JsonIgnore]
public string ShortName
{
get
+20
View File
@@ -70,6 +70,7 @@ namespace osu.Game.Online.Chat
[Resolved]
private UserLookupCache users { get; set; }
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private readonly IBindableList<APIRelation> localUserBlocks = new BindableList<APIRelation>();
private ScheduledDelegate scheduledAck;
@@ -95,6 +96,9 @@ namespace osu.Game.Online.Chat
chatClient.PresenceReceived += () => Schedule(initializeChannels);
chatClient.RequestPresence();
localUser.BindTo(api.LocalUser);
localUser.BindValueChanged(userChanged);
apiState.BindTo(api.State);
apiState.BindValueChanged(_ => SendAck(), true);
@@ -102,6 +106,22 @@ namespace osu.Game.Online.Chat
localUserBlocks.BindCollectionChanged((_, args) => Schedule(() => onBlocksChanged(args)));
}
private void userChanged(ValueChangedEvent<APIUser> userChange)
{
if (userChange.OldValue?.Equals(userChange.NewValue) == true)
return;
CurrentChannel.Value = null;
foreach (var joinedChannel in joinedChannels)
joinedChannel.Joined.Value = false;
joinedChannels.Clear();
// additionally clear the history of last joined channels so that the new user can't reopen the old user's channels
// (would likely fail web-side on perms anyway, but why even get that far)
closedChannels.Clear();
}
/// <summary>
/// Opens a channel or switches to the channel if already opened.
/// </summary>
+2 -2
View File
@@ -24,7 +24,7 @@ namespace osu.Game.Online.Chat
private GameHost host { get; set; } = null!;
[Resolved]
private Clipboard clipboard { get; set; } = null!;
private OsuGame? game { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
@@ -88,7 +88,7 @@ namespace osu.Game.Online.Chat
}
if (dialogOverlay != null && shouldWarn)
dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => clipboard.SetText(url)));
dialogOverlay.Push(new ExternalLinkDialog(url, () => host.OpenUrlExternally(url), () => game?.CopyToClipboard(url)));
else
host.OpenUrlExternally(url);
}
@@ -17,7 +17,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -76,7 +75,7 @@ namespace osu.Game.Online.Leaderboards
private SongSelect songSelect { get; set; }
[Resolved(canBeNull: true)]
private Clipboard clipboard { get; set; }
private OsuGame game { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
@@ -459,7 +458,7 @@ namespace osu.Game.Online.Leaderboards
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = copyableMods));
if (Score.OnlineID > 0)
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => clipboard?.SetText($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}")));
items.Add(new OsuMenuItem(CommonStrings.CopyLink, MenuItemType.Standard, () => game?.CopyToClipboard($@"{api.Endpoints.WebsiteUrl}/scores/{Score.OnlineID}")));
if (Score.Files.Count > 0)
{
@@ -235,15 +235,13 @@ namespace osu.Game.Online.Metadata
{
if (userId == api.LocalUser.Value.OnlineID)
localUserPresence = presence.Value;
else
userPresences[userId] = presence.Value;
userPresences[userId] = presence.Value;
}
else
{
if (userId == api.LocalUser.Value.OnlineID)
localUserPresence = default;
else
userPresences.Remove(userId);
userPresences.Remove(userId);
}
});
@@ -28,14 +28,20 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
public int CurrentRound { get; set; }
/// <summary>
/// The playlist items that were picked as gameplay candidates.
/// The playlist items that were picked as candidates by user.
/// </summary>
/// <remarks>
/// May contain <c>-1</c> when any users picked the "random" playlist item.
/// </remarks>
[Key(2)]
public long[] CandidateItems { get; set; } = [];
/// <summary>
/// The final gameplay candidate.
/// A playlist item from <see cref="CandidateItems"/> that was randomly picked by the server.
/// </summary>
/// <remarks>
/// May be <c>-1</c> to indicate the "random" playlist item was chosen.
/// </remarks>
[Key(3)]
public long CandidateItem { get; set; }
@@ -45,6 +51,15 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
[Key(4)]
public MatchmakingUserList Users { get; set; } = new MatchmakingUserList();
/// <summary>
/// A playlist item from the room's playlist that will be played in the current round.
/// </summary>
/// <remarks>
/// The value of this property may not equal <see cref="CandidateItem"/> or exist in <see cref="CandidateItems"/>.
/// </remarks>
[Key(5)]
public long GameplayItem { get; set; }
/// <summary>
/// Advances to the next round.
/// </summary>
@@ -36,5 +36,11 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
/// </summary>
[Key(3)]
public MatchmakingRoundList Rounds { get; set; } = new MatchmakingRoundList();
/// <summary>
/// The time at which this user abandoned the match.
/// </summary>
[Key(4)]
public DateTimeOffset? AbandonedAt { get; set; }
}
}
@@ -23,42 +23,53 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.Matchmaking
ArgumentNullException.ThrowIfNull(x);
ArgumentNullException.ThrowIfNull(y);
// X appears earlier in the list if it has more points.
if (x.Points > y.Points)
return -1;
int compare = compareAbandonedAt(x, y);
if (compare != 0)
return compare;
// Y appears earlier in the list if it has more points.
if (y.Points > x.Points)
return 1;
compare = comparePoints(x, y);
if (compare != 0)
return compare;
// Tiebreaker 1 (likely): From each user's point-of-view, their earliest and best placement.
compare = compareRoundPlacements(x, y);
if (compare != 0)
return compare;
return compareUserIds(x, y);
}
private int compareAbandonedAt(MatchmakingUser x, MatchmakingUser y)
{
DateTimeOffset xAbandonedAt = x.AbandonedAt ?? DateTimeOffset.MaxValue;
DateTimeOffset yAbandonedAt = y.AbandonedAt ?? DateTimeOffset.MaxValue;
return -xAbandonedAt.CompareTo(yAbandonedAt);
}
private int comparePoints(MatchmakingUser x, MatchmakingUser y)
{
return -x.Points.CompareTo(y.Points);
}
private int compareRoundPlacements(MatchmakingUser x, MatchmakingUser y)
{
for (int r = 1; r <= rounds; r++)
{
MatchmakingRound? xRound;
x.Rounds.RoundsDictionary.TryGetValue(r, out xRound);
x.Rounds.RoundsDictionary.TryGetValue(r, out var xRound);
y.Rounds.RoundsDictionary.TryGetValue(r, out var yRound);
MatchmakingRound? yRound;
y.Rounds.RoundsDictionary.TryGetValue(r, out yRound);
int xPlacement = xRound?.Placement ?? int.MaxValue;
int yPlacement = yRound?.Placement ?? int.MaxValue;
// Nothing to do if both players haven't played this round.
if (xRound == null && yRound == null)
continue;
// X appears later in the list if it hasn't played this round.
if (xRound == null)
return 1;
// Y appears later in the list if it hasn't played this round.
if (yRound == null)
return -1;
// X appears earlier in the list if it has a better placement in the round.
int compare = xRound.Placement.CompareTo(yRound.Placement);
int compare = xPlacement.CompareTo(yPlacement);
if (compare != 0)
return compare;
}
// Tiebreaker 2 (unlikely): User ID.
return 0;
}
private int compareUserIds(MatchmakingUser x, MatchmakingUser y)
{
return x.UserId.CompareTo(y.UserId);
}
}
+51 -27
View File
@@ -79,25 +79,6 @@ namespace osu.Game.Overlays.Chat
highlightedMessage.BindValueChanged(_ => processMessageHighlighting(), true);
}
protected override void Update()
{
base.Update();
long? lastMinutes = null;
for (int i = 0; i < ChatLineFlow.Count; i++)
{
if (ChatLineFlow[i] is ChatLine chatline)
{
long minutes = chatline.Message.Timestamp.ToUnixTimeSeconds() / 60;
chatline.AlternatingBackground = i % 2 == 0;
chatline.RequiresTimestamp = minutes != lastMinutes;
lastMinutes = minutes;
}
}
}
/// <summary>
/// Processes any pending message in <see cref="highlightedMessage"/>.
/// </summary>
@@ -145,19 +126,28 @@ namespace osu.Game.Overlays.Chat
// Add up to last Channel.MAX_HISTORY messages
var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY));
Message lastMessage = chatLines.LastOrDefault()?.Message;
ChatLine lastLine = chatLines.LastOrDefault();
Message lastMessage = lastLine?.Message;
foreach (var message in displayMessages)
{
addDaySeparatorIfRequired(lastMessage, message);
var chatLine = CreateChatLine(message);
ChatLine line = CreateChatLine(message);
if (chatLine != null)
{
ChatLineFlow.Add(chatLine);
lastMessage = message;
}
if (line == null)
continue;
long minutes = line.Message.Timestamp.ToUnixTimeSeconds() / 60;
long? lastMinutes = lastLine?.Message.Timestamp.ToUnixTimeSeconds() / 60;
line.AlternatingBackground = lastLine?.AlternatingBackground == false;
line.RequiresTimestamp = minutes != lastMinutes;
ChatLineFlow.Add(line);
lastMessage = message;
lastLine = line;
}
var staleMessages = chatLines.Where(c => c.LifetimeEnd == double.MaxValue).ToArray();
@@ -232,7 +222,41 @@ namespace osu.Game.Overlays.Chat
private void messageRemoved(Message removed) => Schedule(() =>
{
chatLines.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire();
const double fade_time = 600;
ChatLine removedLine = chatLines.FirstOrDefault(c => c.Message == removed);
if (removedLine == null)
return;
removedLine.FadeColour(Color4.Red, 400).FadeOut(fade_time).Expire();
// Resolve new colours and timestamps resulting from the removal.
this.Delay(fade_time).Schedule(() =>
{
ChatLine lastLine = null;
// Preserve the colours of most-recent messages while updating the ones upwards in the list.
foreach (var line in chatLines.Reverse().Except([removedLine]))
{
if (lastLine != null)
line.AlternatingBackground = !lastLine.AlternatingBackground;
lastLine = line;
}
lastLine = null;
// Timestamps may migrate to more recent messages.
foreach (var line in chatLines.Except([removedLine]))
{
long minutes = line.Message.Timestamp.ToUnixTimeSeconds() / 60;
long? lastMinutes = lastLine?.Message.Timestamp.ToUnixTimeSeconds() / 60;
line.RequiresTimestamp = minutes != lastMinutes;
lastLine = line;
}
});
});
private IEnumerable<ChatLine> chatLines => ChatLineFlow.Children.OfType<ChatLine>();
+2 -13
View File
@@ -20,13 +20,11 @@ using System.Collections.Specialized;
using System.Diagnostics;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Comments.Buttons;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.OSD;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments
@@ -83,10 +81,7 @@ namespace osu.Game.Overlays.Comments
private IAPIProvider api { get; set; } = null!;
[Resolved]
private Clipboard clipboard { get; set; } = null!;
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
private OsuGame? game { get; set; }
public DrawableComment(Comment comment, IReadOnlyList<CommentableMeta> meta)
{
@@ -329,7 +324,7 @@ namespace osu.Game.Overlays.Comments
if (WasDeleted)
makeDeleted();
actionsContainer.AddLink(CommonStrings.ButtonsPermalink, copyUrl);
actionsContainer.AddLink(CommonStrings.ButtonsPermalink, () => game?.CopyToClipboard($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}"));
actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
actionsContainer.AddLink(CommonStrings.ButtonsReply.ToLower(), toggleReply);
actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
@@ -417,12 +412,6 @@ namespace osu.Game.Overlays.Comments
api.Queue(request);
}
private void copyUrl()
{
clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}");
onScreenDisplay?.Display(new CopiedToClipboardToast());
}
private void toggleReply()
{
if (replyEditorContainer.Count == 0)
+21 -1
View File
@@ -29,6 +29,8 @@ namespace osu.Game.Overlays
{
public partial class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent
{
public const double TRACK_DRAG_SEEK_DEBOUNCE = 200;
public IconUsage Icon => OsuIcon.Music;
public LocalisableString Title => NowPlayingStrings.HeaderTitle;
public LocalisableString Description => NowPlayingStrings.HeaderDescription;
@@ -207,7 +209,8 @@ namespace osu.Game.Overlays
Height = progress_height / 2,
FillColour = colours.Yellow,
BackgroundColour = colours.YellowDarker.Opacity(0.5f),
OnSeek = musicController.SeekTo
OnSeek = onSeek,
OnCommit = onCommit,
}
},
},
@@ -221,6 +224,23 @@ namespace osu.Game.Overlays
};
}
private double? lastSeekTime;
private void onSeek(double progress)
{
if (lastSeekTime == null || Time.Current - lastSeekTime > TRACK_DRAG_SEEK_DEBOUNCE)
{
musicController.SeekTo(progress);
lastSeekTime = Time.Current;
}
}
private void onCommit(double progress)
{
musicController.SeekTo(progress);
lastSeekTime = null;
}
private void togglePlaylist()
{
if (playlist == null)
@@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings.Sections.Maintenance;
namespace osu.Game.Overlays.Settings.Sections.General
{
public partial class InstallationSettings : SettingsSubsection
{
protected override LocalisableString Header => GeneralSettingsStrings.InstallationHeader;
[Resolved]
private OsuGame? game { get; set; }
[BackgroundDependencyLoader]
private void load(Storage storage)
{
Add(new SettingsButton
{
Text = GeneralSettingsStrings.OpenOsuFolder,
Keywords = new[] { @"logs", @"files", @"access", "directory" },
Action = () => storage.PresentExternally(),
});
Add(new DangerousSettingsButton
{
Text = GeneralSettingsStrings.ChangeFolderLocation,
Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen()))
});
}
}
}
@@ -0,0 +1,125 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
using osu.Game.Graphics;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
using SharpCompress.Archives.Zip;
namespace osu.Game.Overlays.Settings.Sections.General
{
public partial class QuickActionSettings : SettingsSubsection
{
[Resolved(CanBeNull = true)]
private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; }
[Resolved(CanBeNull = true)]
private OsuGame? game { get; set; }
protected override LocalisableString Header => GeneralSettingsStrings.QuickActionsHeader;
[BackgroundDependencyLoader]
private void load(OsuColour colours, Storage storage)
{
AddRange(new Drawable[]
{
new SettingsButton
{
Text = GeneralSettingsStrings.RunSetupWizard,
Keywords = new[] { @"first run", @"initial", @"getting started", @"import", @"tutorial", @"recommended beatmaps" },
TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription,
Action = () => firstRunSetupOverlay?.Show(),
},
new SettingsButton
{
Text = GeneralSettingsStrings.LearnMoreAboutLazer,
TooltipText = GeneralSettingsStrings.LearnMoreAboutLazerTooltip,
BackgroundColour = colours.YellowDark,
Action = () => game?.ShowWiki(@"Help_centre/Upgrading_to_lazer")
},
new SettingsButton
{
Text = GeneralSettingsStrings.ReportIssue,
TooltipText = GeneralSettingsStrings.ReportIssueTooltip,
BackgroundColour = colours.YellowDarker,
Action = () => game?.OpenUrlExternally(@"https://osu.ppy.sh/community/forums/topics/create?forum_id=5", LinkWarnMode.NeverWarn)
},
});
bool supportsExport = RuntimeInfo.OS != RuntimeInfo.Platform.Android;
if (supportsExport)
{
Add(new SettingsButton
{
Text = GeneralSettingsStrings.ExportLogs,
BackgroundColour = colours.YellowDarker.Darken(0.5f),
Keywords = new[] { @"bug", "report", "logs", "files" },
Action = () => Task.Run(exportLogs),
});
exportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports");
}
}
[Resolved]
private INotificationOverlay? notifications { get; set; }
private Storage exportStorage = null!;
private void exportLogs()
{
ProgressNotification notification = new ProgressNotification
{
State = ProgressNotificationState.Active,
Text = "Exporting logs...",
};
notifications?.Post(notification);
const string archive_filename = "compressed-logs.zip";
try
{
GlobalStatistics.OutputToLog();
Logger.Flush();
var logStorage = Logger.Storage;
using (var outStream = exportStorage.CreateFileSafely(archive_filename))
using (var zip = ZipArchive.Create())
{
foreach (string? f in logStorage.GetFiles(string.Empty, "*.log"))
FileUtils.AttemptOperation(z => z.AddEntry(f, logStorage.GetStream(f), true), zip);
zip.SaveTo(outStream);
}
}
catch
{
notification.State = ProgressNotificationState.Cancelled;
// cleanup if export is failed or canceled.
exportStorage.Delete(archive_filename);
throw;
}
notification.CompletionText = "Exported logs! Click to view.";
notification.CompletionClickAction = () => exportStorage.PresentFileExternally(archive_filename);
notification.State = ProgressNotificationState.Completed;
}
}
}
@@ -7,20 +7,12 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Statistics;
using osu.Game.Configuration;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Updater;
using osu.Game.Utils;
using SharpCompress.Archives.Zip;
namespace osu.Game.Overlays.Settings.Sections.General
{
@@ -45,79 +37,40 @@ namespace osu.Game.Overlays.Settings.Sections.General
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
private Storage exportStorage = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, Storage storage)
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.ReleaseStream, configReleaseStream);
bool isDesktop = RuntimeInfo.IsDesktop;
bool supportsExport = RuntimeInfo.OS != RuntimeInfo.Platform.Android;
bool canCheckUpdates = updateManager?.CanCheckForUpdate == true;
if (canCheckUpdates)
// For simplicity, hide the concept of release streams from mobile users.
if (isDesktop)
{
// For simplicity, hide the concept of release streams from mobile users.
if (isDesktop)
Add(releaseStreamDropdown = new SettingsEnumDropdown<ReleaseStream>
{
Add(releaseStreamDropdown = new SettingsEnumDropdown<ReleaseStream>
{
LabelText = GeneralSettingsStrings.ReleaseStream,
Current = { Value = configReleaseStream.Value },
Keywords = new[] { @"version" },
});
LabelText = GeneralSettingsStrings.ReleaseStream,
Current = { Value = configReleaseStream.Value },
Keywords = new[] { @"version" },
});
if (updateManager!.FixedReleaseStream != null)
{
configReleaseStream.Value = updateManager.FixedReleaseStream.Value;
if (updateManager!.FixedReleaseStream != null)
{
configReleaseStream.Value = updateManager.FixedReleaseStream.Value;
releaseStreamDropdown.ShowsDefaultIndicator = false;
releaseStreamDropdown.Items = [updateManager.FixedReleaseStream.Value];
releaseStreamDropdown.SetNoticeText(GeneralSettingsStrings.ChangeReleaseStreamPackageManagerWarning);
}
releaseStreamDropdown.Current.BindValueChanged(releaseStreamChanged);
releaseStreamDropdown.ShowsDefaultIndicator = false;
releaseStreamDropdown.Items = [updateManager.FixedReleaseStream.Value];
releaseStreamDropdown.SetNoticeText(GeneralSettingsStrings.ChangeReleaseStreamPackageManagerWarning);
}
Add(checkForUpdatesButton = new SettingsButton
{
Text = GeneralSettingsStrings.CheckUpdate,
Action = () => checkForUpdates().FireAndForget()
});
releaseStreamDropdown.Current.BindValueChanged(releaseStreamChanged);
}
// Loosely update-related maintenance buttons.
if (isDesktop)
Add(checkForUpdatesButton = new SettingsButton
{
Add(new SettingsButton
{
Text = GeneralSettingsStrings.OpenOsuFolder,
Keywords = new[] { @"logs", @"files", @"access", "directory" },
Action = () => storage.PresentExternally(),
});
}
if (supportsExport)
{
Add(new SettingsButton
{
Text = GeneralSettingsStrings.ExportLogs,
Keywords = new[] { @"bug", "report", "logs", "files" },
Action = () => Task.Run(exportLogs),
});
}
if (isDesktop)
{
Add(new SettingsButton
{
Text = GeneralSettingsStrings.ChangeFolderLocation,
Action = () => game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen()))
});
}
exportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports");
Text = GeneralSettingsStrings.CheckUpdate,
Action = () => checkForUpdates().FireAndForget()
});
}
private void releaseStreamChanged(ValueChangedEvent<ReleaseStream> stream)
@@ -176,48 +129,5 @@ namespace osu.Game.Overlays.Settings.Sections.General
checkForUpdatesButton.Enabled.Value = true;
}
}
private void exportLogs()
{
ProgressNotification notification = new ProgressNotification
{
State = ProgressNotificationState.Active,
Text = "Exporting logs...",
};
notifications?.Post(notification);
const string archive_filename = "compressed-logs.zip";
try
{
GlobalStatistics.OutputToLog();
Logger.Flush();
var logStorage = Logger.Storage;
using (var outStream = exportStorage.CreateFileSafely(archive_filename))
using (var zip = ZipArchive.Create())
{
foreach (string? f in logStorage.GetFiles(string.Empty, "*.log"))
FileUtils.AttemptOperation(z => z.AddEntry(f, logStorage.GetStream(f), true), zip);
zip.SaveTo(outStream);
}
}
catch
{
notification.State = ProgressNotificationState.Cancelled;
// cleanup if export is failed or canceled.
exportStorage.Delete(archive_filename);
throw;
}
notification.CompletionText = "Exported logs! Click to view.";
notification.CompletionClickAction = () => exportStorage.PresentFileExternally(archive_filename);
notification.State = ProgressNotificationState.Completed;
}
}
}
@@ -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;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
@@ -8,17 +9,12 @@ using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings.Sections.General;
using osu.Game.Updater;
namespace osu.Game.Overlays.Settings.Sections
{
public partial class GeneralSection : SettingsSection
{
[Resolved(CanBeNull = true)]
private FirstRunSetupOverlay? firstRunSetupOverlay { get; set; }
[Resolved(CanBeNull = true)]
private OsuGame? game { get; set; }
public override LocalisableString Header => CommonStrings.General;
public override Drawable CreateIcon() => new SpriteIcon
@@ -27,27 +23,14 @@ namespace osu.Game.Overlays.Settings.Sections
};
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load(UpdateManager? updateManager)
{
Children = new Drawable[]
{
new SettingsButton
{
Text = GeneralSettingsStrings.RunSetupWizard,
Keywords = new[] { @"first run", @"initial", @"getting started", @"import", @"tutorial", @"recommended beatmaps" },
TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription,
Action = () => firstRunSetupOverlay?.Show(),
},
new SettingsButton
{
Text = GeneralSettingsStrings.LearnMoreAboutLazer,
TooltipText = GeneralSettingsStrings.LearnMoreAboutLazerTooltip,
BackgroundColour = colours.YellowDark,
Action = () => game?.ShowWiki(@"Help_centre/Upgrading_to_lazer")
},
new LanguageSettings(),
new UpdateSettings(),
};
Add(new QuickActionSettings());
Add(new LanguageSettings());
if (updateManager?.CanCheckForUpdate == true)
Add(new UpdateSettings());
if (RuntimeInfo.IsDesktop)
Add(new InstallationSettings());
}
}
}
@@ -36,8 +36,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private Bindable<ScalingMode> scalingMode = null!;
private Bindable<Size> sizeFullscreen = null!;
private Bindable<Size> sizeWindowed = null!;
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
private readonly BindableList<Size> resolutionsFullscreen = new BindableList<Size>(new[] { new Size(9999, 9999) });
private readonly BindableList<Size> resolutionsWindowed = new BindableList<Size>();
private readonly Bindable<Size> windowedResolution = new Bindable<Size>();
private readonly IBindable<FullscreenCapability> fullscreenCapability = new Bindable<FullscreenCapability>(FullscreenCapability.Capable);
[Resolved]
@@ -48,12 +51,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private IWindow? window;
private SettingsDropdown<Size> resolutionDropdown = null!;
private SettingsDropdown<Size> resolutionFullscreenDropdown = null!;
private SettingsDropdown<Size> resolutionWindowedDropdown = null!;
private SettingsDropdown<Display> displayDropdown = null!;
private SettingsDropdown<WindowMode> windowModeDropdown = null!;
private SettingsCheckbox minimiseOnFocusLossCheckbox = null!;
private SettingsCheckbox safeAreaConsiderationsCheckbox = null!;
private Bindable<double> windowedPositionX = null!;
private Bindable<double> windowedPositionY = null!;
private Bindable<float> scalingPositionX = null!;
private Bindable<float> scalingPositionY = null!;
private Bindable<float> scalingSizeX = null!;
@@ -70,12 +76,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingMode = osuConfig.GetBindable<ScalingMode>(OsuSetting.Scaling);
sizeFullscreen = config.GetBindable<Size>(FrameworkSetting.SizeFullscreen);
sizeWindowed = config.GetBindable<Size>(FrameworkSetting.WindowedSize);
windowedPositionX = config.GetBindable<double>(FrameworkSetting.WindowedPositionX);
windowedPositionY = config.GetBindable<double>(FrameworkSetting.WindowedPositionY);
scalingSizeX = osuConfig.GetBindable<float>(OsuSetting.ScalingSizeX);
scalingSizeY = osuConfig.GetBindable<float>(OsuSetting.ScalingSizeY);
scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX);
scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
scalingBackgroundDim = osuConfig.GetBindable<float>(OsuSetting.ScalingBackgroundDim);
windowedResolution.Value = sizeWindowed.Value;
if (window != null)
{
currentDisplay.BindTo(window.CurrentDisplayBindable);
@@ -100,13 +111,20 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
Items = window?.Displays,
Current = currentDisplay,
},
resolutionDropdown = new ResolutionSettingsDropdown
resolutionFullscreenDropdown = new ResolutionSettingsDropdown
{
LabelText = GraphicsSettingsStrings.Resolution,
ShowsDefaultIndicator = false,
ItemSource = resolutions,
ItemSource = resolutionsFullscreen,
Current = sizeFullscreen
},
resolutionWindowedDropdown = new ResolutionSettingsDropdown
{
LabelText = GraphicsSettingsStrings.Resolution,
ShowsDefaultIndicator = false,
ItemSource = resolutionsWindowed,
Current = windowedResolution
},
minimiseOnFocusLossCheckbox = new SettingsCheckbox
{
LabelText = GraphicsSettingsStrings.MinimiseOnFocusLoss,
@@ -202,19 +220,68 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{
if (display.NewValue == null)
{
resolutions.Clear();
resolutionsFullscreen.Clear();
resolutionsWindowed.Clear();
return;
}
resolutions.ReplaceRange(1, resolutions.Count - 1, display.NewValue.DisplayModes
.Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
.OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width))
.Select(m => m.Size)
.Distinct());
var buffer = new Bindable<Size>(windowedResolution.Value);
resolutionWindowedDropdown.Current = buffer;
var fullscreenResolutions = display.NewValue.DisplayModes
.Where(m => m.Size.Width >= 800 && m.Size.Height >= 600)
.OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width))
.Select(m => m.Size)
.Distinct()
.ToList();
var windowedResolutions = fullscreenResolutions
.Where(res => res.Width <= display.NewValue.UsableBounds.Width && res.Height <= display.NewValue.UsableBounds.Height)
.ToList();
resolutionsFullscreen.ReplaceRange(1, resolutionsFullscreen.Count - 1, fullscreenResolutions);
resolutionsWindowed.ReplaceRange(0, resolutionsWindowed.Count, windowedResolutions);
resolutionWindowedDropdown.Current = windowedResolution;
updateDisplaySettingsVisibility();
}), true);
windowedResolution.BindValueChanged(size =>
{
if (size.NewValue == sizeWindowed.Value || windowModeDropdown.Current.Value != WindowMode.Windowed)
return;
if (window?.WindowState == Framework.Platform.WindowState.Maximised)
{
window.WindowState = Framework.Platform.WindowState.Normal;
}
// Adjust only for top decorations (assuming system titlebar).
// Bottom/left/right borders are ignored as invisible padding, which don't align with the screen.
var dBounds = currentDisplay.Value.Bounds;
var dUsable = currentDisplay.Value.UsableBounds;
float topBar = host.Window?.BorderSize.Value.Top ?? 0;
int w = Math.Min(size.NewValue.Width, dUsable.Width);
int h = (int)Math.Min(size.NewValue.Height, dUsable.Height - topBar);
windowedResolution.Value = new Size(w, h);
sizeWindowed.Value = windowedResolution.Value;
float adjustedY = Math.Max(
dUsable.Y + (dUsable.Height - h) / 2f,
dUsable.Y + topBar // titlebar adjustment
);
windowedPositionY.Value = dBounds.Height - h != 0 ? (adjustedY - dBounds.Y) / (dBounds.Height - h) : 0;
windowedPositionX.Value = dBounds.Width - w != 0 ? (dUsable.X - dBounds.X + (dUsable.Width - w) / 2f) / (dBounds.Width - w) : 0;
});
sizeWindowed.BindValueChanged(size =>
{
if (size.NewValue != windowedResolution.Value)
windowedResolution.Value = size.NewValue;
});
scalingMode.BindValueChanged(_ =>
{
scalingSettings.ClearTransforms();
@@ -223,8 +290,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
updateScalingModeVisibility();
});
// initial update bypasses transforms
updateScalingModeVisibility();
void updateScalingModeVisibility()
@@ -260,7 +325,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private void updateDisplaySettingsVisibility()
{
resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen;
resolutionFullscreenDropdown.CanBeShown.Value = windowModeDropdown.Current.Value == WindowMode.Fullscreen && resolutionsFullscreen.Count > 1;
resolutionWindowedDropdown.CanBeShown.Value = windowModeDropdown.Current.Value == WindowMode.Windowed && resolutionsWindowed.Count > 1;
displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1;
minimiseOnFocusLossCheckbox.CanBeShown.Value = RuntimeInfo.IsDesktop && windowModeDropdown.Current.Value == WindowMode.Fullscreen;
safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero;
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -139,9 +140,14 @@ namespace osu.Game.Overlays.Settings.Sections.Input
/// </summary>
/// <param name="fullState">A <see cref="KeyCombination"/> generated from the full input state.</param>
/// <param name="triggerKey">The key which triggered this update, and should be used as the binding.</param>
public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) =>
// TODO: Distinct() can be removed after https://github.com/ppy/osu-framework/pull/6130 is merged.
UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey).Distinct().ToArray()));
public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey)
{
var combination = fullState.Keys.Where(KeyCombination.IsModifierKey)
.Append(triggerKey)
.Select(k => k.GetVirtualKey() ?? k)
.ToArray();
UpdateKeyCombination(new KeyCombination(combination));
}
public void UpdateKeyCombination(KeyCombination newCombination)
{
@@ -191,8 +191,18 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Spacing = new Vector2(5),
Children = new Drawable[]
{
new CancelButton { Action = () => finalise(false) },
new ClearButton { Action = clear },
new RoundedButton
{
Text = CommonStrings.ButtonsCancel,
Size = new Vector2(80, 20),
Action = () => finalise(false)
},
new DangerousRoundedButton
{
Text = CommonStrings.ButtonsClear,
Size = new Vector2(80, 20),
Action = clear
},
},
},
new HoverClickSounds()
@@ -538,23 +548,5 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
isDefault.Value = KeyBindings.Select(b => b.KeyCombination).SequenceEqual(Defaults);
}
private partial class CancelButton : RoundedButton
{
public CancelButton()
{
Text = CommonStrings.ButtonsCancel;
Size = new Vector2(80, 20);
}
}
public partial class ClearButton : DangerousRoundedButton
{
public ClearButton()
{
Text = CommonStrings.ButtonsClear;
Size = new Vector2(80, 20);
}
}
}
}
@@ -137,7 +137,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
rotation.BindValueChanged(val =>
{
usableAreaContainer.RotateTo(val.NewValue, 100, Easing.OutQuint);
tabletContainer.RotateTo(-val.NewValue, 800, Easing.OutQuint);
tabletContainer.RotateTo(-val.NewValue, 400, Easing.OutQuint);
checkBounds();
}, true);
@@ -137,7 +137,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Alpha = 0,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 8),
Spacing = new Vector2(0, SettingsSection.ITEM_SPACING),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
+6 -1
View File
@@ -16,7 +16,12 @@ namespace osu.Game.Overlays.Settings
public SettingsButton()
{
RelativeSizeAxes = Axes.X;
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS };
Margin = new MarginPadding { Vertical = -5 };
Padding = new MarginPadding
{
Left = SettingsPanel.CONTENT_MARGINS,
Right = SettingsPanel.CONTENT_MARGINS,
};
}
public IEnumerable<string> Keywords { get; set; } = Array.Empty<string>();
+20 -20
View File
@@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
@@ -46,6 +45,12 @@ namespace osu.Game.Overlays
public BindableBool Expanded { get; } = new BindableBool(true);
public Vector2 Spacing
{
get => content.Spacing;
set => content.Spacing = value;
}
private OsuSpriteText headerText = null!;
private Container headerContent = null!;
@@ -58,6 +63,9 @@ namespace osu.Game.Overlays
private Drawable? draggedChild;
private bool? lastMouseInBounds;
private bool mouseInBounds => Contains(inputManager.CurrentState.Mouse.Position);
/// <summary>
/// Create a new instance.
/// </summary>
@@ -137,20 +145,6 @@ namespace osu.Game.Overlays
this.Delay(600).Schedule(updateFadeState);
}
protected override bool OnHover(HoverEvent e)
{
updateFadeState();
updateExpandedState(true);
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateFadeState();
updateExpandedState(true);
base.OnHoverLost(e);
}
protected override void Update()
{
base.Update();
@@ -160,10 +154,16 @@ namespace osu.Game.Overlays
headerText.Alpha = (float)Interpolation.DampContinuously(headerText.Alpha, headerText.DrawWidth < DrawWidth ? 1 : 0, 40, Time.Elapsed);
// Dragged child finished its drag operation.
if (draggedChild != null && inputManager.DraggedDrawable != draggedChild)
{
bool childDragFinished = draggedChild != null && inputManager.DraggedDrawable != draggedChild;
if (childDragFinished)
draggedChild = null;
if (childDragFinished || lastMouseInBounds != mouseInBounds)
{
updateExpandedState(true);
updateFadeState();
lastMouseInBounds = mouseInBounds;
}
}
@@ -179,7 +179,7 @@ namespace osu.Game.Overlays
// potentially continuing to get processed while content has changed to autosize.
content.ClearTransforms();
if (Expanded.Value || IsHovered || draggedChild != null)
if (Expanded.Value || mouseInBounds || draggedChild != null)
{
content.AutoSizeAxes = Axes.Y;
content.AutoSizeDuration = animate ? transition_duration : 0;
@@ -198,8 +198,8 @@ namespace osu.Game.Overlays
{
const float fade_duration = 500;
background.FadeTo(IsHovered ? 1 : 0.1f, fade_duration, Easing.OutQuint);
expandButton.FadeTo(IsHovered ? 1 : 0, fade_duration, Easing.OutQuint);
background.FadeTo(mouseInBounds ? 1 : 0.1f, fade_duration, Easing.OutQuint);
expandButton.FadeTo(mouseInBounds ? 1 : 0, fade_duration, Easing.OutQuint);
}
}
}
+1
View File
@@ -11,6 +11,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("osu.Game.Tests.Dynamic")]
[assembly: InternalsVisibleTo("osu.Game.Tests.iOS")]
[assembly: InternalsVisibleTo("osu.Game.Tests.Android")]
[assembly: InternalsVisibleTo("osu.Game.Tournament.Tests")]
// intended for Moq usage
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
@@ -20,12 +20,12 @@ using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
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;
using osuTK;
namespace osu.Game.Rulesets.Edit
{
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Edit
Bindable<double> IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
private ExpandableSlider<double, SizeSlider<double>> distanceSpacingSlider = null!;
private ExpandableSlider<double> distanceSpacingSlider = null!;
private ExpandableButton currentDistanceSpacingButton = null!;
[Resolved]
@@ -75,14 +75,16 @@ namespace osu.Game.Rulesets.Edit
toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("snapping")
{
Name = "snapping",
Spacing = new Vector2(5),
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Children = new Drawable[]
{
distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>>
distanceSpacingSlider = new ExpandableSlider<double>
{
KeyboardStep = adjust_step,
// Manual binding in LoadComplete to handle one-way event flow.
Current = DistanceSpacingMultiplier.GetUnboundCopy(),
ExpandedLabelText = "Distance spacing",
},
currentDistanceSpacingButton = new ExpandableButton
{
@@ -104,7 +106,7 @@ namespace osu.Game.Rulesets.Edit
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
{
distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
distanceSpacingSlider.Current.Value = multiplier.NewValue;
if (multiplier.NewValue != multiplier.OldValue)
onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
+1 -1
View File
@@ -11,7 +11,7 @@ using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Rulesets.Edit
{
public partial class ExpandableButton : RoundedButton, IExpandable
public sealed partial class ExpandableButton : RoundedButton, IExpandable
{
private float actualHeight;
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -47,6 +48,8 @@ namespace osu.Game.Rulesets.Edit
private HitObject? getPreviousHitObject() => beatmap.HitObjects.TakeWhile(h => h.StartTime <= startTimeBindable.Value).LastOrDefault();
protected override bool IsValidForPlacement => HitObject.StartTime >= beatmap.ControlPointInfo.TimingPoints[0].Time;
[Resolved]
private IPlacementHandler placementHandler { get; set; } = null!;
@@ -87,6 +90,13 @@ namespace osu.Game.Rulesets.Edit
placementHandler.HidePlacement();
}
protected override void Update()
{
base.Update();
Colour = IsValidForPlacement ? Colour4.White : Colour4.Red;
}
/// <summary>
/// Updates the time and position of this <see cref="PlacementBlueprint"/>.
/// </summary>
@@ -151,7 +151,7 @@ namespace osu.Game.Screens.Edit.Components
});
};
inputTextBox.Current.BindValueChanged(val => editor?.HandleTimestamp(val.NewValue));
inputTextBox.Current.BindValueChanged(val => editor?.HandleTimestamp(val.NewValue.Trim()));
inputTextBox.OnCommit += (_, __) =>
{
@@ -5,7 +5,7 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
@@ -34,39 +34,53 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
});
}
private double? lastSeekTime;
protected override bool OnDragStart(DragStartEvent e) => true;
protected override void OnDrag(DragEvent e)
{
seekToPosition(e.ScreenSpaceMousePosition);
base.OnDrag(e);
seekToPosition(e.ScreenSpaceMousePosition, instant: false);
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
seekToPosition(e.ScreenSpaceMousePosition, instant: true);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
seekToPosition(e.ScreenSpaceMousePosition);
seekToPosition(e.ScreenSpaceMousePosition, instant: true);
return true;
}
private ScheduledDelegate? scheduledSeek;
/// <summary>
/// Seeks the <see cref="SummaryTimeline"/> to the time closest to a position on the screen relative to the <see cref="SummaryTimeline"/>.
/// </summary>
/// <param name="screenPosition">The position in screen coordinates.</param>
private void seekToPosition(Vector2 screenPosition)
/// <param name="instant">Whether the seek should be instant (drag end, mouse button press) or debounced (drag in progress).</param>
private void seekToPosition(Vector2 screenPosition, bool instant)
{
scheduledSeek?.Cancel();
scheduledSeek = Schedule(() =>
{
float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth);
editorClock.SeekSmoothlyTo(markerPos / DrawWidth * editorClock.TrackLength);
});
float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth);
double seekDestination = markerPos / DrawWidth * editorClock.TrackLength;
marker.X = (float)seekDestination;
if (!instant && lastSeekTime != null && Time.Current - lastSeekTime < NowPlayingOverlay.TRACK_DRAG_SEEK_DEBOUNCE)
return;
editorClock.SeekSmoothlyTo(seekDestination);
lastSeekTime = instant ? null : Time.Current;
}
protected override void Update()
{
base.Update();
marker.X = (float)editorClock.CurrentTime;
if (!IsDragged)
marker.X = (float)editorClock.CurrentTime;
}
protected override void LoadBeatmap(EditorBeatmap beatmap)
@@ -138,6 +138,8 @@ namespace osu.Game.Screens.Edit.Compose
// regardless of whether anything was even selected at all.
// UX-wise this is generally strange and unexpected, but make it work anyways to preserve muscle memory.
// note that this means that `getTimestamp()` must handle no-selection case, too.
// additionally, note we're intentionally not using `OsuGame.CopyToClipboard()`
// because we do not want toasts to pop up on every Ctrl-C press - it'd be disruptive to mappers.
hostClipboard.SetText(getTimestamp());
if (CanCopy.Value)
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
@@ -180,10 +181,16 @@ namespace osu.Game.Screens.Edit.GameplayTest
switch (e.Action)
{
case GlobalAction.EditorTestPlayToggleAutoplay:
if (PauseOverlay?.State.Value == Visibility.Visible || DrawableRuleset.ResumeOverlay?.State.Value == Visibility.Visible)
return true;
toggleAutoplay();
return true;
case GlobalAction.EditorTestPlayToggleQuickPause:
if (PauseOverlay?.State.Value == Visibility.Visible || DrawableRuleset.ResumeOverlay?.State.Value == Visibility.Visible)
return true;
toggleQuickPause();
return true;
+3
View File
@@ -286,6 +286,9 @@ namespace osu.Game.Screens.Menu
if (e.Key >= Key.F1 && e.Key <= Key.F35)
return false;
if (e.Key >= Key.Mute && e.Key <= Key.TrackNext)
return false;
switch (e.Key)
{
case Key.Escape:

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