1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-19 15:33:02 +08:00

Compare commits

...

179 Commits

146 changed files with 3405 additions and 1397 deletions
+3
View File
@@ -73,6 +73,9 @@ Aside from the above, below is a brief checklist of things to watch out when you
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
- Please pick the following target branch for your pull request:
- `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets,
- `master`, otherwise.
- Please avoid pushing untested or incomplete code.
- Please do not force-push or rebase unless we ask you to.
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1028.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1121.1" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase("mania-samples")]
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
[TestCase("slider-convert-samples")]
[TestCase("spinner-convert-samples")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
@@ -0,0 +1,10 @@
osu file format v14
[General]
Mode: 0
[TimingPoints]
0,300,4,0,2,100,1,0
[HitObjects]
444,320,1000,5,2,0:0:0:0:
@@ -0,0 +1,16 @@
{
"Mappings": [{
"StartTime": 1000.0,
"Objects": [{
"StartTime": 1000.0,
"EndTime": 8000.0,
"Column": 0,
"PlaySlidingSamples": false,
"NodeSamples": [
["Gameplay/soft-hitnormal"],
["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"]
],
"Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"],
}]
}]
}
@@ -0,0 +1,18 @@
osu file format v14
[General]
Mode: 0
[Difficulty]
HPDrainRate:5
CircleSize:5
OverallDifficulty:5
ApproachRate:5
SliderMultiplier:1.4
SliderTickRate:1
[TimingPoints]
0,500,4,2,0,100,1,0
[HitObjects]
256,192,1000,8,4,8000,0:2:0:0:
@@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
AssertBeatmapLookup(expected_sample);
AssertNoLookup(unwanted_sample);
}
[Test]
public void TestConvertHitObjectCustomSampleBank()
{
const string beatmap_sample = "normal-hitwhistle2";
const string user_skin_sample = "normal-hitnormal";
SetupSkins(beatmap_sample, user_skin_sample);
CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(beatmap_sample);
AssertUserLookup(user_skin_sample);
}
}
}
@@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Duration = endTime - HitObject.StartTime,
Column = column,
Samples = HitObject.Samples,
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
NodeSamples =
[
HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(),
HitObject.Samples
]
};
}
else
@@ -64,11 +64,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private readonly Lazy<bool> hasKeyTexture;
private readonly ManiaBeatmap beatmap;
private readonly bool isBeatmapConverted;
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
: base(skin)
{
this.beatmap = (ManiaBeatmap)beatmap;
isBeatmapConverted = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
hasKeyTexture = new Lazy<bool>(() =>
@@ -196,8 +198,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
public override ISample GetSample(ISampleInfo sampleInfo)
{
// layered hit sounds never play in mania
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
// layered hit sounds never play in mania-native beatmaps (but do play on converts)
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered && !isBeatmapConverted)
return new SampleVirtual();
return base.GetSample(sampleInfo);
@@ -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);
});
}
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
@@ -10,6 +11,7 @@ using osu.Framework.Input.States;
using osu.Framework.Logging;
using osu.Framework.Testing.Input;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
@@ -58,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var smokeContainer in smokeContainers)
{
if (smokeContainer.Children.Count != 0)
if (smokeContainer.Children.OfType<SkinnableDrawable>().Any())
return false;
}
@@ -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;
}
@@ -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:
@@ -142,7 +142,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 +151,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;
@@ -67,6 +67,9 @@ namespace osu.Game.Rulesets.Osu.Mods
{
if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
LastAcceptedAction = null;
if (LastAcceptedAction != null && gameplayClock.IsRewinding)
LastAcceptedAction = null;
}
protected abstract bool CheckValidNewAction(OsuAction action);
@@ -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;
}
@@ -77,9 +77,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
base.LoadComplete();
RelativeSizeAxes = Axes.Both;
}
LifetimeStart = smokeStartTime = Time.Current;
public void StartDrawing(double time)
{
LifetimeStart = smokeStartTime = time;
LifetimeEnd = smokeEndTime = double.MaxValue;
SmokePoints.Clear();
lastPosition = null;
totalDistance = pointInterval;
}
@@ -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()
+15 -6
View File
@@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
@@ -19,17 +19,24 @@ namespace osu.Game.Rulesets.Osu.UI
/// </summary>
public partial class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler<OsuAction>
{
private DrawablePool<SmokeSkinnableDrawable> segmentPool = null!;
private SmokeSkinnableDrawable? currentSegmentSkinnable;
private Vector2 lastMousePosition;
public override bool ReceivePositionalInputAt(Vector2 _) => true;
[BackgroundDependencyLoader]
private void load()
{
AddInternal(segmentPool = new DrawablePool<SmokeSkinnableDrawable>(10));
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
if (e.Action == OsuAction.Smoke)
{
AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment()));
AddInternal(currentSegmentSkinnable = segmentPool.Get(segment => segment.Segment?.StartDrawing(Time.Current)));
// Add initial position immediately.
addPosition();
@@ -59,17 +66,19 @@ namespace osu.Game.Rulesets.Osu.UI
return base.OnMouseMove(e);
}
private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current);
private void addPosition() => currentSegmentSkinnable?.Segment?.AddPosition(lastMousePosition, Time.Current);
private partial class SmokeSkinnableDrawable : SkinnableDrawable
{
public SmokeSegment? Segment => Drawable as SmokeSegment;
public override bool RemoveWhenNotAlive => true;
public override double LifetimeStart => Drawable.LifetimeStart;
public override double LifetimeEnd => Drawable.LifetimeEnd;
public SmokeSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinComponentLookup, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(lookup, defaultImplementation, confineMode)
public SmokeSkinnableDrawable()
: base(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment())
{
}
}
@@ -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;
@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private int rollingHits;
private readonly Container tickContainer;
private SkinnableDrawable headPiece;
private Color4 colourIdle;
private Color4 colourEngaged;
@@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Content.Add(tickContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = float.MinValue
Depth = -1,
});
}
@@ -79,7 +80,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override void RecreatePieces()
{
if (headPiece != null)
Content.Remove(headPiece, true);
base.RecreatePieces();
Content.Add(headPiece = createHeadPiece());
updateColour();
Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE;
}
@@ -122,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollBody),
_ => new ElongatedCirclePiece());
private SkinnableDrawable createHeadPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollHead), _ => Empty())
{
RelativeSizeAxes = Axes.Y,
Depth = -2,
};
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
private void onNewResult(DrawableHitObject obj, JudgementResult result)
@@ -174,7 +187,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private void updateColour(double fadeDuration = 0)
{
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
if (fadeDuration == 0)
{
// fade duration is 0 when calling via `RecreatePieces()`.
// in this case we want to apply the colour *without* using transforms.
// using transforms may result in the application of colour being undone via `DrawableHitObject.UpdateState()` clearing transforms.
if (MainPiece.Drawable is IHasAccentColour mainPieceWithAccentColour)
mainPieceWithAccentColour.AccentColour = newColour;
if (headPiece.Drawable is IHasAccentColour headPieceWithAccentColour)
headPieceWithAccentColour.AccentColour = newColour;
}
else
{
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
(headPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
}
}
public partial class StrongNestedHit : DrawableStrongNestedHit
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -21,14 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
get
{
// the reason why this calculation is so involved is that the head & tail sprites have different sizes/radii.
// therefore naively taking the SSDQs of them and making a quad out of them results in a trapezoid shape and not a box.
var headCentre = headCircle.ScreenSpaceDrawQuad.Centre;
var headCentre = (body.ScreenSpaceDrawQuad.TopLeft + body.ScreenSpaceDrawQuad.BottomLeft) / 2;
var tailCentre = (tailCircle.ScreenSpaceDrawQuad.TopLeft + tailCircle.ScreenSpaceDrawQuad.BottomLeft) / 2;
float headRadius = headCircle.ScreenSpaceDrawQuad.Height / 2;
float tailRadius = tailCircle.ScreenSpaceDrawQuad.Height / 2;
float radius = Math.Max(headRadius, tailRadius);
float radius = body.ScreenSpaceDrawQuad.Height / 2;
var rectangle = new RectangleF(headCentre.X, headCentre.Y, tailCentre.X - headCentre.X, 0).Inflate(radius);
return new Quad(rectangle.TopLeft, rectangle.TopRight, rectangle.BottomLeft, rectangle.BottomRight);
@@ -37,8 +32,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => ScreenSpaceDrawQuad.Contains(screenSpacePos);
private LegacyCirclePiece headCircle = null!;
private Sprite body = null!;
private Sprite tailCircle = null!;
@@ -66,10 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
RelativeSizeAxes = Axes.Both,
Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
},
headCircle = new LegacyCirclePiece
{
RelativeSizeAxes = Axes.Y,
},
};
AccentColour = colours.YellowDark;
@@ -101,7 +90,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
headCircle.AccentColour = colour;
body.Colour = colour;
tailCircle.Colour = colour;
}
@@ -103,6 +103,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
switch (taikoComponent.Component)
{
case TaikoSkinComponents.DrumRollHead:
if (GetTexture("taiko-roll-middle") != null)
return new LegacyCirclePiece();
return null;
case TaikoSkinComponents.DrumRollBody:
if (GetTexture("taiko-roll-middle") != null)
return new LegacyDrumRoll();
@@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Taiko
InputDrum,
CentreHit,
RimHit,
DrumRollHead,
DrumRollBody,
DrumRollTick,
Swell,
@@ -42,6 +42,7 @@ namespace osu.Game.Tests.Chat
sentMessages = new List<Message>();
silencedUserIds = new List<int>();
((DummyAPIAccess)API).LocalUserState.Blocks.Clear();
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
@@ -63,6 +64,10 @@ namespace osu.Game.Tests.Chat
silencedUserIds.Clear();
return true;
case GetMessagesRequest getMessages:
getMessages.TriggerSuccess(sentMessages);
return true;
case GetUpdatesRequest updatesRequest:
updatesRequest.TriggerSuccess(new GetUpdatesResponse
{
@@ -161,6 +166,60 @@ namespace osu.Game.Tests.Chat
AddUntilStep("/help command received", () => channel.Messages.Last().Content.Contains("Supported commands"));
}
[Test]
public void TestBlockedUserMessagesAreDeletedFromInitialMessageBatch()
{
Channel channel = null;
AddStep("create channel", () => channel = createChannel(1, ChannelType.Public));
AddStep("post a message from blocked user", () => sentMessages.Add(new Message
{
ChannelId = channel.Id,
Content = "i am blocked",
SenderId = 1234
}));
AddStep("mark user as blocked", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation
{
TargetUser = new APIUser { Username = "blocked", Id = 1234 },
TargetID = 1234,
}));
AddStep("join channel and select it", () =>
{
channelManager.JoinChannel(channel);
channelManager.CurrentChannel.Value = channel;
});
AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty);
}
[Test]
public void TestBlockedUserMessagesAreDeletedImmediatelyOnBlock()
{
Channel channel = null;
AddStep("create channel", () => channel = createChannel(1, ChannelType.Public));
AddStep("join channel and select it", () =>
{
channelManager.JoinChannel(channel);
channelManager.CurrentChannel.Value = channel;
});
AddStep("post a message from blocked user", () => sentMessages.Add(new Message
{
ChannelId = channel.Id,
Content = "i am blocked",
SenderId = 1234
}));
AddUntilStep("channel has message", () => channel.Messages, () => Is.Not.Empty);
AddStep("block user", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation
{
TargetUser = new APIUser { Username = "blocked", Id = 1234 },
TargetID = 1234,
}));
AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty);
}
private void handlePostMessageRequest(PostMessageRequest request)
{
var message = new Message(++currentMessageId)
@@ -126,6 +126,22 @@ namespace osu.Game.Tests.Gameplay
AssertBeatmapLookup(expected_sample);
}
/// <summary>
/// Tests that a hitobject which specifies a specific sample file which doesn't exist (or isn't allowed to be looked up)
/// falls back to a normal sample.
/// </summary>
[Test]
public void TestFileSampleFallsBackToNormal()
{
const string expected_sample = "normal-hitnormal";
SetupSkins(null, expected_sample);
CreateTestWithBeatmap("file-beatmap-sample.osu");
AssertUserLookup(expected_sample);
}
/// <summary>
/// Tests that a default hitobject and control point causes <see cref="TestDefaultSampleFromUserSkin"/>.
/// </summary>
@@ -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]
@@ -220,10 +220,13 @@ namespace osu.Game.Tests.Visual.Components
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
if (registerAsOwner)
dependencies.CacheAs<IPreviewTrackOwner>(this);
return dependencies;
{
// Automatically handled by interface caching.
return base.CreateChildDependencies(parent);
}
return new DependencyContainer();
}
}
@@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Drawable OverlayContent => InternalChild;
public Drawable FadingContent => (OverlayContent as Container)?.Child;
public new Drawable FadingContent => (OverlayContent as Container)?.Child;
}
}
}
@@ -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));
}
}
}
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -13,15 +14,15 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
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 MultiplayerPlaylistItem[] items = null!;
private MatchmakingPlaylistItem[] items = null!;
private BeatmapSelectGrid grid = null!;
@@ -36,24 +37,44 @@ namespace osu.Game.Tests.Visual.Matchmaking
.Take(50)
.ToArray();
IEnumerable<MatchmakingPlaylistItem> playlistItems;
if (beatmaps.Length > 0)
{
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
playlistItems = Enumerable.Range(1, 50).Select(i =>
{
ID = i,
BeatmapID = beatmaps[i % beatmaps.Length].OnlineID,
StarRating = i / 10.0,
}).ToArray();
var beatmap = beatmaps[i % beatmaps.Length];
return new MatchmakingPlaylistItem(
new MultiplayerPlaylistItem
{
ID = i,
BeatmapID = beatmap.OnlineID,
StarRating = i / 10.0,
},
CreateAPIBeatmap(beatmap),
Array.Empty<Mod>()
);
});
}
else
{
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
{
ID = i,
BeatmapID = i,
StarRating = i / 10.0,
}).ToArray();
playlistItems = Enumerable.Range(1, 50).Select(i => new MatchmakingPlaylistItem(
new MultiplayerPlaylistItem
{
ID = i,
BeatmapID = i,
StarRating = i / 10.0,
},
CreateAPIBeatmap(),
Array.Empty<Mod>()
));
}
foreach (var item in playlistItems)
item.Beatmap.StarRating = item.PlaylistItem.StarRating;
items = playlistItems.ToArray();
}
public override void SetUpSteps()
@@ -70,8 +91,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("add items", () =>
{
foreach (var item in items)
grid.AddItem(item);
grid.AddItems(items);
});
AddWaitStep("wait for panels", 3);
@@ -85,17 +105,17 @@ namespace osu.Game.Tests.Visual.Matchmaking
// test scene is weird.
});
AddStep("add selection 1", () => grid.ChildrenOfType<BeatmapSelectPanel>().First().AddUser(new APIUser
AddStep("add selection 1", () => grid.ChildrenOfType<MatchmakingSelectPanel>().First().AddUser(new APIUser
{
Id = DummyAPIAccess.DUMMY_USER_ID,
Username = "Maarvin",
}));
AddStep("add selection 2", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(5).First().AddUser(new APIUser
AddStep("add selection 2", () => grid.ChildrenOfType<MatchmakingSelectPanel>().Skip(5).First().AddUser(new APIUser
{
Id = 2,
Username = "peppy",
}));
AddStep("add selection 3", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(10).First().AddUser(new APIUser
AddStep("add selection 3", () => grid.ChildrenOfType<MatchmakingSelectPanel>().Skip(10).First().AddUser(new APIUser
{
Id = 1040328,
Username = "smoogipoo",
@@ -109,7 +129,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
var (candidateItems, finalItem) = pickRandomItems(5);
grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem);
grid.RollAndDisplayFinalBeatmap(candidateItems, finalItem, finalItem);
});
}
@@ -138,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);
});
}
@@ -153,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);
});
}
@@ -180,7 +218,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("display roll order", () =>
{
var panels = grid.ChildrenOfType<BeatmapSelectPanel>().ToArray();
var panels = grid.ChildrenOfType<MatchmakingSelectPanel>().ToArray();
for (int i = 0; i < panels.Length; i++)
{
@@ -1,37 +1,54 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
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);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () =>
{
var room = CreateDefaultRoom(MatchType.Matchmaking);
room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem
{
ID = i,
BeatmapID = 0,
StarRating = i / 10.0,
})).ToArray();
JoinRoom(room);
});
}
[Test]
public void TestBeatmapPanel()
{
BeatmapSelectPanel? panel = null;
MatchmakingSelectPanel? panel = null;
AddStep("add panel", () =>
{
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), []))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -58,47 +75,55 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("remove peppy", () => panel!.RemoveUser(new APIUser { Id = 2 }));
AddStep("remove maarvin", () => panel!.RemoveUser(new APIUser { Id = 6411631 }));
AddToggleStep("allow selection", value =>
{
if (panel != null)
panel.AllowSelection = value;
});
AddToggleStep("allow selection", value => panel!.AllowSelection = value);
}
[Test]
public void TestFailedBeatmapLookup()
public void TestRandomPanel()
{
AddStep("setup request handle", () =>
{
var api = (DummyAPIAccess)API;
var handler = api.HandleRequest;
api.HandleRequest = req =>
{
switch (req)
{
case GetBeatmapRequest:
case GetBeatmapsRequest:
req.TriggerFailure(new InvalidOperationException());
return false;
default:
return handler?.Invoke(req) ?? false;
}
};
});
MatchmakingSelectPanelRandom? panel = null;
AddStep("add panel", () =>
{
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
Child = panel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
});
AddToggleStep("allow selection", value => panel!.AllowSelection = value);
AddStep("reveal beatmap", () => panel!.PresentAsChosenBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [])));
}
[Test]
public void TestBeatmapWithMods()
{
AddStep("add panel", () =>
{
MatchmakingSelectPanel? panel;
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [new OsuModHardRock(), new OsuModDoubleTime()]))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
panel.AddUser(new APIUser
{
Id = 2,
Username = "peppy",
});
});
}
}
}
@@ -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;
@@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
base.SetUpSteps();
AddStep("load screen", () => LoadScreen(new IntroScreen()));
AddStep("load screen", () => LoadScreen(new ScreenIntro()));
}
[Test]
@@ -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!;
@@ -27,7 +26,13 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined();
AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(1)
AddStep("join other player to room", () => MultiplayerClient.AddUser(new APIUser
{
Id = 2,
Username = "peppy",
}));
AddStep("add panel", () => Child = panel = new PlayerPanel(new MultiplayerRoomUser(2)
{
User = new APIUser
{
@@ -85,9 +90,9 @@ namespace osu.Game.Tests.Visual.Matchmaking
UserDictionary =
{
{
1, new MatchmakingUser
2, new MatchmakingUser
{
UserId = 1,
UserId = 2,
Placement = 1,
Points = ++points
}
@@ -100,7 +105,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
[Test]
public void TestJump()
{
AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(1, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely());
AddStep("jump", () => MultiplayerClient.SendUserMatchRequest(2, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely());
}
[Test]
@@ -108,5 +113,14 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
AddToggleStep("toggle quit", quit => panel.HasQuit = quit);
}
[Test]
public void TestDownloadProgress()
{
AddStep("set download progress 20%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.2f)));
AddStep("set download progress 50%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.5f)));
AddStep("set download progress 90%", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.Downloading(0.9f)));
AddStep("set locally available", () => MultiplayerClient.ChangeUserBeatmapAvailability(2, BeatmapAvailability.LocallyAvailable()));
}
}
}
@@ -8,17 +8,18 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Matchmaking.Events;
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!;
@@ -158,5 +159,64 @@ namespace osu.Game.Tests.Visual.Matchmaking
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
}
[Test]
public void InteractionSpam()
{
AddStep("join users", () =>
{
for (int i = 0; i < 7; i++)
{
MultiplayerClient.AddUser(new MultiplayerRoomUser(i)
{
User = new APIUser
{
Username = $"User {i}"
}
});
}
});
AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid);
AddStep("player jump", () => { MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely(); });
AddStep("local jumping", () => jumpSpam(false));
AddWaitStep("wait", 25);
AddStep("group jumping spam", () => jumpSpam(true));
AddWaitStep("wait", 25);
AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split);
AddStep("local jumping", () => jumpSpam(false));
AddWaitStep("wait", 25);
AddStep("group jumping spam", () => jumpSpam(true));
AddWaitStep("wait", 25);
AddStep("change to hidden mode", () => list.DisplayStyle = PanelDisplayStyle.Hidden);
AddStep("local jumping", () => jumpSpam(false));
AddWaitStep("wait", 25);
AddStep("group jumping spam", () => jumpSpam(true));
AddWaitStep("wait", 25);
}
private void jumpSpam(bool everyone)
{
for (int i = 0; i < 30; i++)
{
Scheduler.AddDelayed(() =>
{
MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely();
}, i * 150 + RNG.NextDouble(0, 140));
if (!everyone)
continue;
for (int ii = 0; ii < 7; ii++)
{
int iii = ii;
Scheduler.AddDelayed(() =>
{
MultiplayerClient.SendUserMatchRequest(iii, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely();
}, i * 150 + RNG.NextDouble(0, 140));
}
}
}
}
}
@@ -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()
{
@@ -152,5 +151,32 @@ namespace osu.Game.Tests.Visual.Matchmaking
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
}
[Test]
public void TestUserWithNoScore()
{
AddStep("join another user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(2)
{
User = new APIUser
{
Id = 2,
Username = "Other user"
}
}));
AddStep("show results with no score", () =>
{
var state = new MatchmakingRoomState
{
CurrentRound = 6,
Stage = MatchmakingStage.Ended
};
state.Users.GetOrAdd(API.LocalUser.Value.OnlineID).Rounds.GetOrAdd(1).Placement = 1;
state.Users.GetOrAdd(2);
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
}
}
}
@@ -14,12 +14,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()
{
@@ -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);
@@ -0,0 +1,73 @@
// 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.Extensions;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerSkipOverlay : MultiplayerTestScene
{
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
WaitForJoined();
AddStep("add skip overlay", () =>
{
GameplayClockContainer gameplayClockContainer;
var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new MultiplayerSkipOverlay(120000)
},
};
gameplayClockContainer.Start();
});
AddStep("set playing state", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Playing));
}
[Test]
public void TestSkip()
{
for (int i = 0; i < 4; i++)
{
int i2 = i;
AddStep($"join user {i2}", () =>
{
MultiplayerClient.AddUser(new APIUser
{
Id = i2,
Username = $"User {i2}"
});
MultiplayerClient.ChangeUserState(i2, MultiplayerUserState.Playing);
});
}
AddStep("local user votes", () => MultiplayerClient.VoteToSkipIntro().WaitSafely());
for (int i = 0; i < 4; i++)
{
int i2 = i;
AddStep($"user {i2} votes", () => MultiplayerClient.UserVoteToSkipIntro(i2).WaitSafely());
}
}
}
}
@@ -0,0 +1,67 @@
// 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.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Tests.Visual.UserInterface;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneGlobalRankDisplay : ThemeComparisonTestScene
{
public TestSceneGlobalRankDisplay()
: base(false)
{
}
protected override Drawable CreateContent() => new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Full,
Padding = new MarginPadding(20),
Spacing = new Vector2(40),
ChildrenEnumerable = new int?[] { 64, 423, 1_453, 3_468, 8_367, 48_342, 78_432, 375_231, 897_783, null }.Select(createDisplay)
};
private GlobalRankDisplay createDisplay(int? rank) => new GlobalRankDisplay
{
UserStatistics =
{
Value = new UserStatistics
{
GlobalRank = rank,
GlobalRankPercent = rank / 1_000_000f,
Variants =
[
new UserStatistics.Variant
{
VariantType = UserStatistics.RulesetVariant.FourKey,
GlobalRank = rank / 3,
},
new UserStatistics.Variant
{
VariantType = UserStatistics.RulesetVariant.SevenKey,
GlobalRank = 2 * rank / 3,
}
]
},
},
HighestRank =
{
Value = rank == null
? null
: new APIUser.UserRankHighest
{
Rank = rank.Value / 2,
UpdatedAt = DateTimeOffset.Now.AddMonths(-3),
}
}
};
}
}
@@ -4,9 +4,12 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Carousel;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.SongSelectV2
@@ -322,5 +325,38 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SelectNextSet();
AddUntilStep("no beatmap panels visible", () => GetVisiblePanels<PanelBeatmap>().Count(), () => Is.Zero);
}
[Test]
public void TestGroupChangedAfterEngagingArtistGrouping()
{
RemoveAllBeatmaps();
AddStep("add test beatmaps", () =>
{
for (int i = 0; i < 5; ++i)
{
var baseTestBeatmap = TestResources.CreateTestBeatmapSetInfo(3);
var metadata = new BeatmapMetadata
{
Artist = $"{(char)('A' + i)} artist",
Title = $"{(char)('A' + 4 - i)} title",
};
foreach (var b in baseTestBeatmap.Beatmaps)
b.Metadata = metadata;
Realm.Write(r => r.Add(baseTestBeatmap, update: true));
BeatmapSets.Add(baseTestBeatmap.Detach());
}
SortAndGroupBy(SortMode.Title, GroupMode.Title);
SelectNextSet();
SelectNextSet();
WaitForExpandedGroup(1);
SortAndGroupBy(SortMode.Artist, GroupMode.Artist);
WaitForExpandedGroup(3);
});
}
}
}
@@ -2,9 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
@@ -13,25 +16,35 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneDrawableDate : OsuTestScene
{
public TestSceneDrawableDate()
[SetUpSteps]
public void SetUpSteps()
{
Child = new FillFlowContainer
AddStep("Create 7 dates", () =>
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new Drawable[]
Child = new FillFlowContainer
{
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))),
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))),
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))),
new PokeyDrawableDate(DateTimeOffset.Now),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))),
}
};
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new Drawable[]
{
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(60))),
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(55))),
new PokeyDrawableDate(DateTimeOffset.Now.Subtract(TimeSpan.FromSeconds(50))),
new PokeyDrawableDate(DateTimeOffset.Now),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(60))),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(65))),
new PokeyDrawableDate(DateTimeOffset.Now.Add(TimeSpan.FromSeconds(70))),
}
};
});
}
[Test]
public void TestSecondsUpdate()
{
AddUntilStep("4th date says \"2 seconds ago\"", () => this.ChildrenOfType<DrawableDate>().ElementAt(3).Current.Value == "2 seconds ago");
}
private partial class PokeyDrawableDate : CompositeDrawable
@@ -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);
});
@@ -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,
@@ -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);
+3
View File
@@ -1,6 +1,8 @@
// 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;
namespace osu.Game.Audio
{
/// <summary>
@@ -10,6 +12,7 @@ namespace osu.Game.Audio
/// <see cref="IPreviewTrackOwner"/>s can cancel the currently playing <see cref="PreviewTrack"/> through the
/// global <see cref="PreviewTrackManager"/> if they're the owner of the playing <see cref="PreviewTrack"/>.
/// </remarks>
[Cached]
public interface IPreviewTrackOwner
{
}
+6
View File
@@ -157,6 +157,12 @@ namespace osu.Game.Beatmaps
public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b);
public override int GetHashCode()
{
// ReSharper disable once NonReadonlyMemberInGetHashCode
return ID.GetHashCode();
}
public bool AudioEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
@@ -544,7 +544,7 @@ namespace osu.Game.Beatmaps.Formats
if (!banksOnly)
{
int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty;
string sampleFilename = samples.FirstOrDefault(s => s is ConvertHitObjectParser.FileHitSampleInfo)?.LookupNames.First() ?? string.Empty;
int volume = samples.FirstOrDefault()?.Volume ?? 100;
// We want to ignore custom sample banks and volume when not encoding to the mania game mode,
+3 -2
View File
@@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
@@ -263,11 +264,11 @@ namespace osu.Game.Collections
{
Debug.Assert(collection != null);
collection.PerformWrite(c =>
Task.Run(() => collection.PerformWrite(c =>
{
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
});
}));
}
protected override Drawable CreateContent() => (Content)base.CreateContent();
@@ -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.Threading.Tasks;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
@@ -10,7 +11,7 @@ namespace osu.Game.Collections
public class CollectionToggleMenuItem : ToggleMenuItem
{
public CollectionToggleMenuItem(Live<BeatmapCollection> collection, IBeatmapInfo beatmap)
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state =>
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state => Task.Run(() =>
{
collection.PerformWrite(c =>
{
@@ -19,7 +20,7 @@ namespace osu.Game.Collections
else
c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash);
});
})
}))
{
State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash));
}
+30 -21
View File
@@ -785,32 +785,20 @@ namespace osu.Game.Graphics.Carousel
// We are performing two important operations here:
// - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions.
// - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use.
FindCarouselItemsForSelection(ref currentKeyboardSelection, ref currentSelection, carouselItems);
for (int i = 0; i < count; i++)
{
var item = carouselItems[i];
bool isKeyboardSelection = CheckModelEquality(item.Model, currentKeyboardSelection.Model!);
bool isSelection = CheckModelEquality(item.Model, currentSelection.Model!);
// while we don't know the Y position of the item yet, as it's about to be updated,
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
// at the correct item to avoid redundant local equality checks.
// the Y positions will be filled in after they're computed.
if (isKeyboardSelection)
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, null, i);
if (isSelection)
currentSelection = new Selection(currentSelection.Model, item, null, i);
updateItemYPosition(item, ref lastVisible, ref yPos);
if (isKeyboardSelection)
currentKeyboardSelection = currentKeyboardSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
if (isSelection)
currentSelection = currentSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
}
if (currentKeyboardSelection.CarouselItem is CarouselItem currentKeyboardSelectionItem)
currentKeyboardSelection = currentKeyboardSelection with { YPosition = currentKeyboardSelectionItem.CarouselYPosition + currentKeyboardSelectionItem.DrawHeight / 2 };
if (currentSelection.CarouselItem is CarouselItem currentSelectionItem)
currentSelection = currentSelection with { YPosition = currentSelectionItem.CarouselYPosition + currentSelectionItem.DrawHeight / 2 };
// Update the total height of all items (to make the scroll container scrollable through the full height even though
// most items are not displayed / loaded).
Scroll.SetLayoutHeight(yPos + visibleHalfHeight);
@@ -821,6 +809,27 @@ namespace osu.Game.Graphics.Carousel
Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value));
}
protected virtual void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList<CarouselItem> items)
{
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
bool isKeyboardSelection = CheckModelEquality(item.Model, keyboardSelection.Model!);
bool isSelection = CheckModelEquality(item.Model, selection.Model!);
// while we don't know the Y position of the item yet, as it's about to be updated,
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
// at the correct item to avoid redundant local equality checks.
// the Y positions will be filled in after they're computed.
if (isKeyboardSelection)
keyboardSelection = new Selection(keyboardSelection.Model, item, null, i);
if (isSelection)
selection = new Selection(selection.Model, item, null, i);
}
}
#endregion
#region Display handling
@@ -1081,7 +1090,7 @@ namespace osu.Game.Graphics.Carousel
/// <param name="CarouselItem">A related carousel item representation for the model. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
/// <param name="YPosition">The Y position of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
/// <param name="Index">The index of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
protected record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
private record DisplayRange(int First, int Last)
{
@@ -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;
}
}
}
}
@@ -15,7 +15,6 @@ using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers
{
[Cached(typeof(IPreviewTrackOwner))]
public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction>
{
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
+14 -6
View File
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Utils;
@@ -80,7 +81,7 @@ namespace osu.Game.Graphics
public DateTimeOffset TooltipContent => Date;
private class HumanisedDate : IEquatable<HumanisedDate>, ILocalisableStringData
private class HumanisedDate : ILocalisableStringData
{
public readonly DateTimeOffset Date;
@@ -89,11 +90,18 @@ namespace osu.Game.Graphics
Date = date;
}
public bool Equals(HumanisedDate? other)
=> other?.Date != null && Date.Equals(other.Date);
public bool Equals(ILocalisableStringData? other)
=> other is HumanisedDate otherDate && Equals(otherDate);
/// <remarks>
/// Humanizer formats the <see cref="Date"/> relative to the local computer time.
/// Therefore, replacing a <see cref="HumanisedDate"/> instance with another instance of the class with the same <see cref="Date"/>
/// should have the effect of replacing and re-formatting the text.
/// Including <see cref="Date"/> in equality members would stop this from happening, as <see cref="SpriteText.Text"/>
/// has equality-based early guards to prevent redundant text replaces.
/// Thus, instances of these class just compare <see langword="false"/> to any <see cref="ILocalisableStringData"/> to ensure re-formatting happens correctly.
/// There are "technically" more "correct" ways to do this (like also including the current time into equality checks),
/// but they are simultaneously functionally equivalent to this and overly convoluted.
/// This is a private hack-job of a wrapper around humanizer anyway.
/// </remarks>
public bool Equals(ILocalisableStringData? other) => false;
public string GetLocalised(LocalisationParameters parameters) => HumanizerUtils.Humanize(Date);
@@ -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>
{
}
+16 -1
View File
@@ -13,7 +13,10 @@ namespace osu.Game.Graphics.UserInterface
{
public partial class ProgressBar : SliderBar<double>
{
public bool Seeking { get; private set; }
public Action<double> OnSeek;
public Action<double> OnCommit;
private readonly Box fill;
private readonly Box background;
@@ -75,6 +78,18 @@ namespace osu.Game.Graphics.UserInterface
fill.Width = value * UsableWidth;
}
protected override void OnUserChange(double value) => OnSeek?.Invoke(value);
protected override void OnUserChange(double value)
{
Seeking = true;
OnSeek?.Invoke(value);
base.OnUserChange(value);
}
protected override bool Commit()
{
Seeking = false;
OnCommit?.Invoke(CurrentNumber.Value);
return base.Commit();
}
}
}
@@ -54,7 +54,13 @@ namespace osu.Game.Graphics.UserInterface
{
case Key.KeypadEnter:
case Key.Enter:
return false;
// even if committing per se is not allowed for this textbox,
// the commit flow is also responsible for terminating any active IME.
// ensure that the Enter press terminates IME correctly
// and is also handled if it needs to be, so that it doesn't leak to some other non-focused drawable and cause breakage.
bool wasImeComposing = ImeCompositionActive;
FinalizeImeComposition(true);
return wasImeComposing;
}
}
@@ -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;
@@ -99,6 +99,21 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString AdjustBeatmapOffsetAutomaticallyTooltip => new TranslatableString(getKey(@"adjust_beatmap_offset_automatically_tooltip"), @"If enabled, the offset suggested from last play on a beatmap is automatically applied.");
/// <summary>
/// "Use experimental audio mode"
/// </summary>
public static LocalisableString WasapiLabel => new TranslatableString(getKey(@"wasapi_label"), @"Use experimental audio mode");
/// <summary>
/// "This will attempt to initialise the audio engine in a lower latency mode."
/// </summary>
public static LocalisableString WasapiTooltip => new TranslatableString(getKey(@"wasapi_tooltip"), @"This will attempt to initialise the audio engine in a lower latency mode.");
/// <summary>
/// "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value."
/// </summary>
public static LocalisableString WasapiNotice => new TranslatableString(getKey(@"wasapi_notice"), @"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
+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}";
}
}
@@ -79,6 +79,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>
@@ -139,11 +139,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();
@@ -44,7 +44,7 @@ namespace osu.Game.Online.API.Requests
private class VerificationFailureResponse
{
[JsonProperty("method")]
public SessionVerificationMethod RequiredSessionVerificationMethod { get; set; }
public SessionVerificationMethod? RequiredSessionVerificationMethod { get; set; }
}
}
}
+19 -1
View File
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -70,6 +71,7 @@ namespace osu.Game.Online.Chat
private UserLookupCache users { get; set; }
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private readonly IBindableList<APIRelation> localUserBlocks = new BindableList<APIRelation>();
private ScheduledDelegate scheduledAck;
private IChatClient chatClient = null!;
@@ -95,6 +97,9 @@ namespace osu.Game.Online.Chat
apiState.BindTo(api.State);
apiState.BindValueChanged(_ => SendAck(), true);
localUserBlocks.BindTo(api.LocalUserState.Blocks);
localUserBlocks.BindCollectionChanged((_, args) => Schedule(() => onBlocksChanged(args)));
}
/// <summary>
@@ -311,8 +316,9 @@ namespace osu.Game.Online.Chat
private void addMessages(List<Message> messages)
{
var channels = JoinedChannels.ToList();
var blockedUserIds = localUserBlocks.Select(b => b.TargetID).ToList();
foreach (var group in messages.GroupBy(m => m.ChannelId))
foreach (var group in messages.Where(m => !blockedUserIds.Contains(m.SenderId)).GroupBy(m => m.ChannelId))
channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
lastSilenceMessageId ??= messages.LastOrDefault()?.Id;
@@ -641,6 +647,18 @@ namespace osu.Game.Online.Chat
api.Queue(req);
}
private void onBlocksChanged(NotifyCollectionChangedEventArgs args)
{
if (args.Action != NotifyCollectionChangedAction.Add)
return;
foreach (APIRelation newBlock in args.NewItems!)
{
foreach (var channel in joinedChannels)
channel.RemoveMessagesFromUser(newBlock.TargetID);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@@ -43,11 +43,15 @@ namespace osu.Game.Online.Matchmaking
/// <summary>
/// The user has raised a candidate playlist item to be played.
/// </summary>
/// <param name="userId">The notifying user.</param>
/// <param name="playlistItemId">The playlist item candidate raised, or -1 as a special value that indicates a random selection.</param>
Task MatchmakingItemSelected(int userId, long playlistItemId);
/// <summary>
/// The user has removed a candidate playlist item.
/// </summary>
/// <param name="userId">The notifying user.</param>
/// <param name="playlistItemId">The playlist item candidate removed, or -1 as a special value that indicates a random selection.</param>
Task MatchmakingItemDeselected(int userId, long playlistItemId);
}
}
@@ -45,7 +45,7 @@ namespace osu.Game.Online.Matchmaking
/// <summary>
/// Raise a candidate playlist item to be played in the current round.
/// </summary>
/// <param name="playlistItemId">The playlist item.</param>
/// <param name="playlistItemId">The playlist item, or -1 to indicate a random selection.</param>
Task MatchmakingToggleSelection(long playlistItemId);
/// <summary>
@@ -149,5 +149,15 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
/// <param name="item">The changed item.</param>
Task PlaylistItemChanged(MultiplayerPlaylistItem item);
/// <summary>
/// Signals that a user has requested to skip the beatmap intro.
/// </summary>
Task UserVotedToSkipIntro(int userId);
/// <summary>
/// Signals that the vote to skip the beatmap intro has passed.
/// </summary>
Task VoteToSkipIntroPassed();
}
}
@@ -112,6 +112,11 @@ namespace osu.Game.Online.Multiplayer
/// <param name="playlistItemId">The item to remove.</param>
Task RemovePlaylistItem(long playlistItemId);
/// <summary>
/// Votes to skip the beatmap intro.
/// </summary>
Task VoteToSkipIntro();
/// <summary>
/// Invites a player to the current room.
/// </summary>
@@ -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>
@@ -131,6 +131,11 @@ namespace osu.Game.Online.Multiplayer
public event Action<int, long>? MatchmakingItemDeselected;
public event Action<MatchRoomState>? MatchRoomStateChanged;
public event Action<int>? UserVotedToSkipIntro;
public event Action? VoteToSkipIntroPassed;
public event Action<MultiplayerRoomUser, BeatmapAvailability>? BeatmapAvailabilityChanged;
/// <summary>
/// Whether the <see cref="MultiplayerClient"/> is currently connected.
/// This is NOT thread safe and usage should be scheduled.
@@ -493,6 +498,8 @@ namespace osu.Game.Online.Multiplayer
public abstract Task RemovePlaylistItem(long playlistItemId);
public abstract Task VoteToSkipIntro();
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
{
handleRoomRequest(() =>
@@ -770,6 +777,7 @@ namespace osu.Game.Online.Multiplayer
user.BeatmapAvailability = beatmapAvailability;
BeatmapAvailabilityChanged?.Invoke(user, beatmapAvailability);
RoomUpdated?.Invoke();
});
@@ -846,6 +854,10 @@ namespace osu.Game.Online.Multiplayer
handleRoomRequest(() =>
{
Debug.Assert(Room != null);
foreach (var user in Room.Users)
user.VotedToSkipIntro = false;
GameplayStarted?.Invoke();
});
@@ -916,6 +928,37 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
Task IMultiplayerClient.UserVotedToSkipIntro(int userId)
{
handleRoomRequest(() =>
{
Debug.Assert(Room != null);
var user = Room.Users.SingleOrDefault(u => u.UserID == userId);
// TODO: user should NEVER be null here, see https://github.com/ppy/osu/issues/17713.
if (user == null)
return;
user.VotedToSkipIntro = true;
UserVotedToSkipIntro?.Invoke(userId);
});
return Task.CompletedTask;
}
Task IMultiplayerClient.VoteToSkipIntroPassed()
{
handleRoomRequest(() =>
{
Debug.Assert(Room != null);
VoteToSkipIntroPassed?.Invoke();
});
return Task.CompletedTask;
}
/// <summary>
/// Populates the <see cref="APIUser"/> for a given collection of <see cref="MultiplayerRoomUser"/>s.
/// </summary>
@@ -49,6 +49,12 @@ namespace osu.Game.Online.Multiplayer
[Key(6)]
public int? BeatmapId;
/// <summary>
/// Whether this user voted to skip the beatmap intro.
/// </summary>
[Key(7)]
public bool VotedToSkipIntro;
[IgnoreMember]
public APIUser? User { get; set; }
@@ -70,7 +70,8 @@ namespace osu.Game.Online.Multiplayer
connection.On<MultiplayerPlaylistItem>(nameof(IMultiplayerClient.PlaylistItemAdded), ((IMultiplayerClient)this).PlaylistItemAdded);
connection.On<long>(nameof(IMultiplayerClient.PlaylistItemRemoved), ((IMultiplayerClient)this).PlaylistItemRemoved);
connection.On<MultiplayerPlaylistItem>(nameof(IMultiplayerClient.PlaylistItemChanged), ((IMultiplayerClient)this).PlaylistItemChanged);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested);
connection.On<int>(nameof(IMultiplayerClient.UserVotedToSkipIntro), ((IMultiplayerClient)this).UserVotedToSkipIntro);
connection.On(nameof(IMultiplayerClient.VoteToSkipIntroPassed), ((IMultiplayerClient)this).VoteToSkipIntroPassed);
connection.On(nameof(IMatchmakingClient.MatchmakingQueueJoined), ((IMatchmakingClient)this).MatchmakingQueueJoined);
connection.On(nameof(IMatchmakingClient.MatchmakingQueueLeft), ((IMatchmakingClient)this).MatchmakingQueueLeft);
@@ -80,6 +81,8 @@ namespace osu.Game.Online.Multiplayer
connection.On<MatchmakingQueueStatus>(nameof(IMatchmakingClient.MatchmakingQueueStatusChanged), ((IMatchmakingClient)this).MatchmakingQueueStatusChanged);
connection.On<int, long>(nameof(IMatchmakingClient.MatchmakingItemSelected), ((IMatchmakingClient)this).MatchmakingItemSelected);
connection.On<int, long>(nameof(IMatchmakingClient.MatchmakingItemDeselected), ((IMatchmakingClient)this).MatchmakingItemDeselected);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested);
};
IsConnected.BindTo(connector.IsConnected);
@@ -312,6 +315,16 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId);
}
public override Task VoteToSkipIntro()
{
if (!IsConnected.Value)
return Task.CompletedTask;
Debug.Assert(connection != null);
return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkipIntro));
}
public override Task DisconnectInternal()
{
if (connector == null)
@@ -150,7 +150,7 @@ namespace osu.Game.Online
// compare: https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/Online/BanchoClient.cs#L539
retryDelay = Math.Min(120000, (int)(retryDelay * 1.5));
Logger.Log($"{ClientName} connect attempt failed: {exception.Message}. Next attempt in {thisDelay / 1000:N0} seconds.", LoggingTarget.Network);
Logger.Log($"{ClientName} connect attempt failed. Next attempt in {thisDelay / 1000:N0} seconds.\n{exception}", LoggingTarget.Network);
await Task.Delay(thisDelay, cancellationToken).ConfigureAwait(false);
}
+5 -2
View File
@@ -162,16 +162,17 @@ namespace osu.Game.Overlays
private int runningDepth;
private readonly Scheduler postScheduler = new Scheduler();
private readonly Scheduler criticalPostScheduler = new Scheduler();
public override bool IsPresent =>
// Delegate presence as we need to consider the toast tray in addition to the main overlay.
State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks;
State.Value == Visibility.Visible || mainContent.IsPresent || toastTray.IsPresent || postScheduler.HasPendingTasks || criticalPostScheduler.HasPendingTasks;
private bool processingPosts = true;
private double? lastSamplePlayback;
public void Post(Notification notification) => postScheduler.Add(() =>
public void Post(Notification notification) => (notification.IsCritical ? criticalPostScheduler : postScheduler).Add(() =>
{
++runningDepth;
@@ -220,6 +221,8 @@ namespace osu.Game.Overlays
{
base.Update();
criticalPostScheduler.Update();
if (processingPosts)
postScheduler.Update();
}
@@ -91,7 +91,12 @@ namespace osu.Game.Overlays
public void FlushAllToasts()
{
foreach (var notification in toastFlow.ToArray())
{
if (notification.IsCritical)
continue;
forwardNotification(notification);
}
}
public void Post(Notification notification)
@@ -39,6 +39,11 @@ namespace osu.Game.Overlays.Notifications
/// </summary>
public bool IsImportant { get; init; } = true;
/// <summary>
/// Critical notifications show even during gameplay or other scenarios where notifications would usually be suppressed.
/// </summary>
public bool IsCritical { get; init; }
/// <summary>
/// Transient notifications only show as a toast, and do not linger in notification history.
/// </summary>
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Notifications
protected override void Update()
{
base.Update();
IconContent.Width = IconContent.DrawHeight;
IconContent.Width = Math.Min(78, IconContent.DrawHeight);
}
}
}
+34 -11
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)
@@ -304,18 +324,21 @@ namespace osu.Game.Overlays
var track = musicController.CurrentTrack;
if (!track.IsDummyDevice)
if (!progressBar.Seeking)
{
progressBar.EndTime = track.Length;
progressBar.CurrentTime = track.CurrentTime;
if (!track.IsDummyDevice)
{
progressBar.EndTime = track.Length;
progressBar.CurrentTime = track.CurrentTime;
playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle;
}
else
{
progressBar.CurrentTime = 0;
progressBar.EndTime = 1;
playButton.Icon = FontAwesome.Regular.PlayCircle;
playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle;
}
else
{
progressBar.CurrentTime = 0;
progressBar.EndTime = 1;
playButton.Icon = FontAwesome.Regular.PlayCircle;
}
}
}
+19 -1
View File
@@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -36,6 +38,7 @@ namespace osu.Game.Overlays
public ScrollBackButton Button { get; private set; }
private readonly Bindable<double?> lastScrollTarget = new Bindable<double?>();
private readonly Bindable<double> progress = new Bindable<double>();
[BackgroundDependencyLoader]
private void load()
@@ -46,7 +49,8 @@ namespace osu.Game.Overlays
Origin = Anchor.BottomRight,
Margin = new MarginPadding(20),
Action = scrollBack,
LastScrollTarget = { BindTarget = lastScrollTarget }
LastScrollTarget = { BindTarget = lastScrollTarget },
Progress = { BindTarget = progress },
});
}
@@ -54,6 +58,10 @@ namespace osu.Game.Overlays
{
base.UpdateAfterChildren();
// Map current position to standardized progress
float height = AvailableContent - DrawHeight;
progress.Value = height == 0 ? 1 : Math.Round(Math.Clamp(Current / height, 0, 1), 3);
if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight)
{
Button.State = Visibility.Hidden;
@@ -110,9 +118,11 @@ namespace osu.Game.Overlays
private readonly Container content;
private readonly Box background;
private readonly CircularProgress currentCircularProgress;
private readonly SpriteIcon spriteIcon;
public Bindable<double?> LastScrollTarget = new Bindable<double?>();
public Bindable<double> Progress = new Bindable<double>();
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
@@ -145,6 +155,11 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both
},
currentCircularProgress = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
InnerRadius = 0.1f,
},
spriteIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
@@ -164,6 +179,7 @@ namespace osu.Game.Overlays
IdleColour = colourProvider.Background6;
HoverColour = colourProvider.Background5;
flashColour = colourProvider.Light1;
currentCircularProgress.Colour = colourProvider.Highlight1;
scrollToTopSample = audio.Samples.Get(@"UI/scroll-to-top");
scrollToPreviousSample = audio.Samples.Get(@"UI/scroll-to-previous");
@@ -173,6 +189,8 @@ namespace osu.Game.Overlays
{
base.LoadComplete();
Progress.BindValueChanged(p => currentCircularProgress.Progress = p.NewValue, true);
LastScrollTarget.BindValueChanged(target =>
{
spriteIcon.ScaleTo(target.NewValue != null ? new Vector2(1f, -1f) : Vector2.One, fade_duration, Easing.OutQuint);
@@ -0,0 +1,137 @@
// 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.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class GlobalRankDisplay : CompositeDrawable
{
public Bindable<UserStatistics?> UserStatistics = new Bindable<UserStatistics?>();
public Bindable<APIUser.UserRankHighest?> HighestRank = new Bindable<APIUser.UserRankHighest?>();
private ProfileValueDisplay info = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public GlobalRankDisplay()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = info = new ProfileValueDisplay(big: true)
{
Title = UsersStrings.ShowRankGlobalSimple
};
}
protected override void LoadComplete()
{
base.LoadComplete();
UserStatistics.BindValueChanged(_ => updateState());
HighestRank.BindValueChanged(_ => updateState(), true);
}
private void updateState()
{
info.Content.Text = UserStatistics.Value?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
info.Content.TooltipText = getGlobalRankTooltipText();
var tier = getRankingTier();
info.Content.Colour = tier == null ? colourProvider.Content2 : OsuColour.ForRankingTier(tier.Value);
info.Content.Font = info.Content.Font.With(weight: tier == null || tier == RankingTier.Iron ? FontWeight.Regular : FontWeight.Bold);
}
/// <seealso href="https://github.com/ppy/osu-web/blob/6fcd85eb006ce7699d6f747597435c01344b2d2d/resources/js/profile-page/rank.tsx#L19-L46"/>
private RankingTier? getRankingTier()
{
var stats = UserStatistics.Value;
int? rank = stats?.GlobalRank;
float? percent = stats?.GlobalRankPercent;
if (rank == null || percent == null)
return null;
if (rank <= 100)
return RankingTier.Lustrous;
if (percent < 0.0005)
return RankingTier.Radiant;
if (percent < 0.0015)
return RankingTier.Rhodium;
if (percent < 0.005)
return RankingTier.Platinum;
if (percent < 0.015)
return RankingTier.Gold;
if (percent < 0.05)
return RankingTier.Silver;
if (percent < 0.15)
return RankingTier.Bronze;
if (percent < 0.5)
return RankingTier.Iron;
return null;
}
private LocalisableString getGlobalRankTooltipText()
{
var rankHighest = HighestRank.Value;
var variants = UserStatistics.Value?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
if (variant.GlobalRank != null)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
}
if (rankHighest != null)
{
var rankHighestText = UsersStrings.ShowRankHighest(
rankHighest.Rank.ToLocalisableString("\\##,##0"),
rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"));
if (result == null)
result = rankHighestText;
else
result = LocalisableString.Interpolate($"{result}\n{rankHighestText}");
}
return result ?? default;
}
}
}
@@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private readonly Dictionary<ScoreRank, ScoreRankInfo> scoreRankInfos = new Dictionary<ScoreRank, ScoreRankInfo>();
private ProfileValueDisplay medalInfo = null!;
private ProfileValueDisplay ppInfo = null!;
private ProfileValueDisplay detailGlobalRank = null!;
private GlobalRankDisplay detailGlobalRank = null!;
private ProfileValueDisplay detailCountryRank = null!;
private RankGraph rankGraph = null!;
@@ -64,10 +64,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
new[]
{
detailGlobalRank = new ProfileValueDisplay(true)
{
Title = UsersStrings.ShowRankGlobalSimple,
},
detailGlobalRank = new GlobalRankDisplay(),
Empty(),
detailCountryRank = new ProfileValueDisplay(true)
{
@@ -156,59 +153,22 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
var user = data?.User;
medalInfo.Content = user?.Achievements?.Length.ToString() ?? "0";
ppInfo.Content = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0";
medalInfo.Content.Text = user?.Achievements?.Length.ToString() ?? "0";
ppInfo.Content.Text = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0";
ppInfo.Content.TooltipText = getPPInfoTooltipText(user);
foreach (var scoreRankInfo in scoreRankInfos)
scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0;
detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
detailGlobalRank.ContentTooltipText = getGlobalRankTooltipText(user);
detailGlobalRank.HighestRank.Value = user?.RankHighest;
detailGlobalRank.UserStatistics.Value = user?.Statistics;
detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
detailCountryRank.ContentTooltipText = getCountryRankTooltipText(user);
detailCountryRank.Content.Text = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
detailCountryRank.Content.TooltipText = getCountryRankTooltipText(user);
rankGraph.Statistics.Value = user?.Statistics;
}
private static LocalisableString getGlobalRankTooltipText(APIUser? user)
{
var rankHighest = user?.RankHighest;
var variants = user?.Statistics?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
if (variant.GlobalRank != null)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
}
if (rankHighest != null)
{
var rankHighestText = UsersStrings.ShowRankHighest(
rankHighest.Rank.ToLocalisableString("\\##,##0"),
rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"));
if (result == null)
result = rankHighestText;
else
result = LocalisableString.Interpolate($"{result}\n{rankHighestText}");
}
return result ?? default;
}
private static LocalisableString getCountryRankTooltipText(APIUser? user)
{
var variants = user?.Statistics?.Variants;
@@ -234,6 +194,28 @@ namespace osu.Game.Overlays.Profile.Header.Components
return result ?? default;
}
private static LocalisableString getPPInfoTooltipText(APIUser? user)
{
var variants = user?.Statistics?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.PP.ToLocalisableString("#,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
return result ?? default;
}
private partial class ScoreRankInfo : CompositeDrawable
{
private readonly OsuSpriteText rankCount;
@@ -14,22 +14,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
public partial class ProfileValueDisplay : CompositeDrawable
{
private readonly OsuSpriteText title;
private readonly ContentText content;
public LocalisableString Title
{
set => title.Text = value;
}
public LocalisableString Content
{
set => content.Text = value;
}
public LocalisableString ContentTooltipText
{
set => content.TooltipText = value;
}
public ContentText Content { get; }
public ProfileValueDisplay(bool big = false, int minimumWidth = 60)
{
@@ -44,9 +35,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
Font = OsuFont.GetFont(size: 12)
},
content = new ContentText
Content = new ContentText
{
Font = OsuFont.GetFont(size: big ? 30 : 20, weight: FontWeight.Light),
Font = OsuFont.GetFont(size: big ? 30 : 20, weight: big ? FontWeight.Regular : FontWeight.Light),
},
new Container // Add a minimum size to the FillFlowContainer
{
@@ -60,10 +51,10 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void load(OverlayColourProvider colourProvider)
{
title.Colour = colourProvider.Content1;
content.Colour = colourProvider.Content2;
Content.Colour = colourProvider.Content2;
}
private partial class ContentText : OsuSpriteText, IHasTooltip
public partial class ContentText : OsuSpriteText, IHasTooltip
{
public LocalisableString TooltipText { get; set; }
}
@@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
InternalChild = info = new ProfileValueDisplay(minimumWidth: 140)
{
Title = UsersStrings.ShowStatsPlayTime,
ContentTooltipText = "0 hours",
Content = { TooltipText = "0 hours", }
};
User.BindValueChanged(updateTime, true);
@@ -35,8 +35,8 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateTime(ValueChangedEvent<UserProfileData?> user)
{
int? playTime = user.NewValue?.User.Statistics?.PlayTime;
info.ContentTooltipText = (playTime ?? 0) / 3600 + " hours";
info.Content = formatTime(playTime);
info.Content.TooltipText = (playTime ?? 0) / 3600 + " hours";
info.Content.Text = formatTime(playTime);
}
private string formatTime(int? secondsNull)
@@ -41,8 +41,8 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
Add(wasapiExperimental = new SettingsCheckbox
{
LabelText = "Use experimental audio mode",
TooltipText = "This will attempt to initialise the audio engine in a lower latency mode.",
LabelText = AudioSettingsStrings.WasapiLabel,
TooltipText = AudioSettingsStrings.WasapiTooltip,
Current = audio.UseExperimentalWasapi,
Keywords = new[] { "wasapi", "latency", "exclusive" }
});
@@ -64,10 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
if (wasapiExperimental != null)
{
if (wasapiExperimental.Current.Value)
{
wasapiExperimental.SetNoticeText(
"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.", true);
}
wasapiExperimental.SetNoticeText(AudioSettingsStrings.WasapiNotice, true);
else
wasapiExperimental.ClearNoticeText();
}
@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Settings.Sections.General;
namespace osu.Game.Overlays.Settings.Sections
@@ -45,6 +46,13 @@ namespace osu.Game.Overlays.Settings.Sections
BackgroundColour = colours.YellowDark,
Action = () => game?.ShowWiki(@"Help_centre/Upgrading_to_lazer")
},
new SettingsButton
{
Text = GeneralSettingsStrings.ReportIssue,
TooltipText = GeneralSettingsStrings.ReportIssueTooltip,
BackgroundColour = colours.DarkOrange2,
Action = () => game?.OpenUrlExternally(@"https://osu.ppy.sh/community/forums/topics/create?forum_id=5", LinkWarnMode.NeverWarn)
},
new LanguageSettings(),
new UpdateSettings(),
};
@@ -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;
@@ -46,6 +46,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!;
@@ -91,6 +91,7 @@ namespace osu.Game.Overlays.SkinEditor
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
config.BindWith(OsuSetting.HUDVisibilityMode, configVisibilityMode);
}
protected override void LoadComplete()
@@ -117,7 +118,7 @@ namespace osu.Game.Overlays.SkinEditor
protected override void PopIn()
{
globallyDisableBeatmapSkinSetting();
overrideSkinEditorRelevantSettings();
if (skinEditor != null)
{
@@ -159,7 +160,7 @@ namespace osu.Game.Overlays.SkinEditor
nestedInputManagerDisable?.Dispose();
nestedInputManagerDisable = null;
globallyReenableBeatmapSkinSetting();
restoreSkinEditorRelevantSettings();
}
public void PresentGameplay() => presentGameplay(false);
@@ -330,24 +331,33 @@ namespace osu.Game.Overlays.SkinEditor
private readonly Bindable<bool> beatmapSkins = new Bindable<bool>();
private LeasedBindable<bool>? leasedBeatmapSkins;
private void globallyDisableBeatmapSkinSetting()
{
if (beatmapSkins.Disabled)
return;
private readonly Bindable<HUDVisibilityMode> configVisibilityMode = new Bindable<HUDVisibilityMode>();
private LeasedBindable<HUDVisibilityMode>? leasedVisibilityMode;
// The skin editor doesn't work well if beatmap skins are being applied to the player screen.
// To keep things simple, disable the setting game-wide while using the skin editor.
//
// This causes a full reload of the skin, which is pretty ugly.
// TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap.
leasedBeatmapSkins = beatmapSkins.BeginLease(true);
leasedBeatmapSkins.Value = false;
private void overrideSkinEditorRelevantSettings()
{
if (!beatmapSkins.Disabled)
{
// The skin editor doesn't work well if beatmap skins are being applied to the player screen.
// To keep things simple, disable the setting game-wide while using the skin editor.
//
// This causes a full reload of the skin, which is pretty ugly.
// TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap.
leasedBeatmapSkins = beatmapSkins.BeginLease(true);
leasedBeatmapSkins.Value = false;
}
leasedVisibilityMode = configVisibilityMode.BeginLease(true);
leasedVisibilityMode.Value = HUDVisibilityMode.Always;
}
private void globallyReenableBeatmapSkinSetting()
private void restoreSkinEditorRelevantSettings()
{
leasedBeatmapSkins?.Return();
leasedBeatmapSkins = null;
leasedVisibilityMode?.Return();
leasedVisibilityMode = null;
}
public new void ToggleVisibility()
+70
View File
@@ -5,13 +5,20 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Overlays.Wiki
{
@@ -19,11 +26,15 @@ namespace osu.Game.Overlays.Wiki
{
public static LocalisableString IndexPageString => LayoutStrings.HeaderHelpIndex;
private const string github_wiki_base = @"https://github.com/ppy/osu-wiki/blob/master/wiki";
public readonly Bindable<APIWikiPage> WikiPageData = new Bindable<APIWikiPage>();
public Action ShowIndexPage;
public Action ShowParentPage;
private readonly Bindable<string> githubPath = new Bindable<string>();
public WikiHeader()
{
TabControl.AddItem(IndexPageString);
@@ -35,6 +46,9 @@ namespace osu.Game.Overlays.Wiki
private void onWikiPageChange(ValueChangedEvent<APIWikiPage> e)
{
// Clear the path beforehand in case we got an error page.
githubPath.Value = null;
if (e.NewValue == null)
return;
@@ -42,6 +56,7 @@ namespace osu.Game.Overlays.Wiki
Current.Value = null;
TabControl.AddItem(IndexPageString);
githubPath.Value = $"{github_wiki_base}/{e.NewValue.Path}/{e.NewValue.Locale}.md";
if (e.NewValue.Path == WikiOverlay.INDEX_PATH)
{
@@ -56,6 +71,27 @@ namespace osu.Game.Overlays.Wiki
Current.Value = e.NewValue.Title;
}
protected override Drawable CreateTabControlContent()
{
return new FillFlowContainer
{
Height = 40,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new ShowOnGitHubButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(32),
TargetPath = { BindTarget = githubPath },
},
},
};
}
private void onCurrentChange(ValueChangedEvent<LocalisableString?> e)
{
if (e.NewValue == TabControl.Items.LastOrDefault())
@@ -83,5 +119,39 @@ namespace osu.Game.Overlays.Wiki
Icon = OsuIcon.Wiki;
}
}
private partial class ShowOnGitHubButton : RoundedButton
{
public override LocalisableString TooltipText => WikiStrings.ShowEditLink;
public readonly Bindable<string> TargetPath = new Bindable<string>();
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] ILinkHandler linkHandler)
{
Width = 42;
Add(new SpriteIcon
{
Size = new Vector2(12),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.Brands.Github,
});
Action = () => linkHandler?.HandleLink(TargetPath.Value);
}
protected override void LoadComplete()
{
base.LoadComplete();
TargetPath.BindValueChanged(e =>
{
this.FadeTo(e.NewValue != null ? 1 : 0);
Enabled.Value = e.NewValue != null;
}, true);
}
}
}
}
+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));
@@ -550,7 +550,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
else
{
// Todo: This should set the normal SampleInfo if the specified sample file isn't found, but that's a pretty edge-case scenario
soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume));
}
@@ -680,14 +679,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered);
}
private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
public class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
{
public readonly string Filename;
public FileHitSampleInfo(string filename, int volume)
// Force CSS=1 to make sure that the LegacyBeatmapSkin does not fall back to the user skin.
// Note that this does not change the lookup names, as they are overridden locally.
: base(string.Empty, customSampleBank: 1, volume: volume)
: base(HIT_NORMAL, SampleControlPoint.DEFAULT_BANK, customSampleBank: 1, volume: volume)
{
Filename = filename;
}
@@ -696,7 +694,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
Filename,
Path.ChangeExtension(Filename, null)
};
}.Concat(base.LookupNames);
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)
@@ -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)

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