1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 12:42:54 +08:00

Merge remote-tracking branch 'OliBomby/fix-segment-ends' into fix-slider-reversing

This commit is contained in:
Pasi4K5 2023-08-19 02:38:34 +02:00
commit 47d787b359
53 changed files with 726 additions and 537 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.801.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.815.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -75,7 +75,7 @@ namespace osu.Desktop.LegacyIpc
case LegacyIpcDifficultyCalculationRequest req: case LegacyIpcDifficultyCalculationRequest req:
try try
{ {
WorkingBeatmap beatmap = new FlatFileWorkingBeatmap(req.BeatmapFile); WorkingBeatmap beatmap = new FlatWorkingBeatmap(req.BeatmapFile);
var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray(); Mod[] mods = ruleset.ConvertFromLegacyMods((LegacyMods)req.Mods).ToArray();

View File

@ -85,7 +85,7 @@ namespace osu.Desktop
} }
} }
using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = true })) using (DesktopGameHost host = Host.GetSuitableDesktopHost(gameName, new HostOptions { BindIPC = !tournamentClient }))
{ {
if (!host.IsPrimaryInstance) if (!host.IsPrimaryInstance)
{ {

View File

@ -6,7 +6,6 @@ using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
@ -24,21 +23,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
Assert.False(testBeatmap.HitObjects.OfType<HoldNote>().Any()); Assert.False(testBeatmap.HitObjects.OfType<HoldNote>().Any());
} }
[Test]
public void TestCorrectNoteValues()
{
var testBeatmap = createRawBeatmap();
var noteValues = new List<double>(testBeatmap.HitObjects.OfType<HoldNote>().Count());
foreach (HoldNote h in testBeatmap.HitObjects.OfType<HoldNote>())
{
noteValues.Add(ManiaModHoldOff.GetNoteDurationInBeatLength(h, testBeatmap));
}
noteValues.Sort();
Assert.AreEqual(noteValues, new List<double> { 0.125, 0.250, 0.500, 1.000, 2.000 });
}
[Test] [Test]
public void TestCorrectObjectCount() public void TestCorrectObjectCount()
{ {
@ -47,25 +31,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
var rawBeatmap = createRawBeatmap(); var rawBeatmap = createRawBeatmap();
var testBeatmap = createModdedBeatmap(); var testBeatmap = createModdedBeatmap();
// Calculate expected number of objects // Both notes and hold notes account for at least one object
int expectedObjectCount = 0; int expectedObjectCount = rawBeatmap.HitObjects.Count;
foreach (ManiaHitObject h in rawBeatmap.HitObjects)
{
// Both notes and hold notes account for at least one object
expectedObjectCount++;
if (h.GetType() == typeof(HoldNote))
{
double noteValue = ManiaModHoldOff.GetNoteDurationInBeatLength((HoldNote)h, rawBeatmap);
if (noteValue >= ManiaModHoldOff.END_NOTE_ALLOW_THRESHOLD)
{
// Should generate an end note if it's longer than the minimum note value
expectedObjectCount++;
}
}
}
Assert.That(testBeatmap.HitObjects.Count == expectedObjectCount); Assert.That(testBeatmap.HitObjects.Count == expectedObjectCount);
} }

View File

@ -117,18 +117,16 @@ namespace osu.Game.Rulesets.Mania.Tests
private void createBarLine(bool major) private void createBarLine(bool major)
{ {
foreach (var stage in stages) var obj = new BarLine
{ {
var obj = new BarLine StartTime = Time.Current + 2000,
{ Major = major,
StartTime = Time.Current + 2000, };
Major = major,
};
obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); obj.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
foreach (var stage in stages)
stage.Add(obj); stage.Add(obj);
}
} }
private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action) private ScrollingTestContainer createStage(ScrollingDirection direction, ManiaAction action)

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{ {
private const double individual_decay_base = 0.125; private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30; private const double overall_decay_base = 0.30;
private const double release_threshold = 24; private const double release_threshold = 30;
protected override double SkillMultiplier => 1; protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1; protected override double StrainDecayBase => 1;
@ -50,10 +50,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
for (int i = 0; i < endTimes.Length; ++i) for (int i = 0; i < endTimes.Length; ++i)
{ {
// The current note is overlapped if a previous note or end is overlapping the current note body // The current note is overlapped if a previous note or end is overlapping the current note body
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1); isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) &&
Precision.DefinitelyBigger(endTime, endTimes[i], 1) &&
Precision.DefinitelyBigger(startTime, startTimes[i], 1);
// We give a slight bonus to everything if something is held meanwhile // We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1)) if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) &&
Precision.DefinitelyBigger(startTime, startTimes[i], 1))
holdFactor = 1.25; holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i])); closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
@ -70,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
// 0.0 +--------+-+---------------> Release Difference / ms // 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold // release_threshold
if (isOverlapping) if (isOverlapping)
holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime))); holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime)));
// Decay and increase individualStrains in own column // Decay and increase individualStrains in own column
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);

View File

@ -33,5 +33,6 @@ namespace osu.Game.Rulesets.Mania
HitExplosion, HitExplosion,
StageBackground, StageBackground,
StageForeground, StageForeground,
BarLine
} }
} }

View File

@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) }; public override Type[] IncompatibleMods => new[] { typeof(ManiaModInvert) };
public const double END_NOTE_ALLOW_THRESHOLD = 0.5;
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {
var maniaBeatmap = (ManiaBeatmap)beatmap; var maniaBeatmap = (ManiaBeatmap)beatmap;
@ -46,28 +44,9 @@ namespace osu.Game.Rulesets.Mania.Mods
StartTime = h.StartTime, StartTime = h.StartTime,
Samples = h.GetNodeSamples(0) Samples = h.GetNodeSamples(0)
}); });
// Don't add an end note if the duration is shorter than the threshold
double noteValue = GetNoteDurationInBeatLength(h, maniaBeatmap); // 1/1, 1/2, 1/4, etc.
if (noteValue >= END_NOTE_ALLOW_THRESHOLD)
{
newObjects.Add(new Note
{
Column = h.Column,
StartTime = h.EndTime,
Samples = h.GetNodeSamples((h.NodeSamples?.Count - 1) ?? 1)
});
}
} }
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList(); maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
} }
public static double GetNoteDurationInBeatLength(HoldNote holdNote, ManiaBeatmap beatmap)
{
double beatLength = beatmap.ControlPointInfo.TimingPointAt(holdNote.StartTime).BeatLength;
return holdNote.Duration / beatLength;
}
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -8,7 +9,15 @@ namespace osu.Game.Rulesets.Mania.Objects
{ {
public class BarLine : ManiaHitObject, IBarLine public class BarLine : ManiaHitObject, IBarLine
{ {
public bool Major { get; set; } private HitObjectProperty<bool> major;
public Bindable<bool> MajorBindable => major.Bindable;
public bool Major
{
get => major.Value;
set => major.Value = value;
}
public override Judgement CreateJudgement() => new IgnoreJudgement(); public override Judgement CreateJudgement() => new IgnoreJudgement();
} }

View File

@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Mania.Skinning.Default;
using osuTK; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -13,45 +15,41 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public partial class DrawableBarLine : DrawableManiaHitObject<BarLine> public partial class DrawableBarLine : DrawableManiaHitObject<BarLine>
{ {
public readonly Bindable<bool> Major = new Bindable<bool>();
public DrawableBarLine()
: this(null!)
{
}
public DrawableBarLine(BarLine barLine) public DrawableBarLine(BarLine barLine)
: base(barLine) : base(barLine)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = barLine.Major ? 1.7f : 1.2f; }
AddInternal(new Box [BackgroundDependencyLoader]
private void load()
{
AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine())
{ {
Name = "Bar line", Anchor = Anchor.Centre,
Anchor = Anchor.BottomCentre, Origin = Anchor.Centre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Alpha = barLine.Major ? 0.5f : 0.2f
}); });
if (barLine.Major) Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true);
{ }
Vector2 size = new Vector2(22, 6);
const float line_offset = 4;
AddInternal(new Circle protected override void OnApply()
{ {
Name = "Left line", base.OnApply();
Anchor = Anchor.CentreLeft, Major.BindTo(HitObject.MajorBindable);
Origin = Anchor.CentreRight, }
Size = size, protected override void OnFree()
X = -line_offset, {
}); base.OnFree();
Major.UnbindFrom(HitObject.MajorBindable);
AddInternal(new Circle
{
Name = "Right line",
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
Size = size,
X = line_offset,
});
}
} }
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150); protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);

View File

@ -0,0 +1,72 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Default
{
public partial class DefaultBarLine : CompositeDrawable
{
private Bindable<bool> major = null!;
private Drawable mainLine = null!;
private Drawable leftAnchor = null!;
private Drawable rightAnchor = null!;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject)
{
RelativeSizeAxes = Axes.Both;
AddInternal(mainLine = new Box
{
Name = "Bar line",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
});
Vector2 size = new Vector2(22, 6);
const float line_offset = 4;
AddInternal(leftAnchor = new Circle
{
Name = "Left anchor",
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Size = size,
X = -line_offset,
});
AddInternal(rightAnchor = new Circle
{
Name = "Right anchor",
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
Size = size,
X = line_offset,
});
major = ((DrawableBarLine)drawableHitObject).Major.GetBoundCopy();
}
protected override void LoadComplete()
{
base.LoadComplete();
major.BindValueChanged(updateMajor, true);
}
private void updateMajor(ValueChangedEvent<bool> major)
{
mainLine.Alpha = major.NewValue ? 0.5f : 0.2f;
leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? 1 : 0;
}
}
}

View File

@ -119,6 +119,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
case ManiaSkinComponents.StageForeground: case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground(); return new LegacyStageForeground();
case ManiaSkinComponents.BarLine:
return null; // Not yet implemented.
default: default:
throw new UnsupportedSkinComponentException(lookup); throw new UnsupportedSkinComponentException(lookup);
} }

View File

@ -30,15 +30,15 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
get get
{ {
if (Stages.Count == 1) RectangleF totalArea = RectangleF.Empty;
return Stages.First().ScreenSpaceDrawQuad;
RectangleF area = RectangleF.Empty; for (int i = 0; i < Stages.Count; ++i)
{
var stageArea = Stages[i].ScreenSpaceDrawQuad.AABBFloat;
totalArea = i == 0 ? stageArea : RectangleF.Union(totalArea, stageArea);
}
foreach (var stage in Stages) return totalArea;
area = RectangleF.Union(area, stage.ScreenSpaceDrawQuad.AABBFloat);
return area;
} }
} }

View File

@ -136,6 +136,8 @@ namespace osu.Game.Rulesets.Mania.UI
columnFlow.SetContentForColumn(i, column); columnFlow.SetContentForColumn(i, column);
AddNested(column); AddNested(column);
} }
RegisterPool<BarLine, DrawableBarLine>(50, 200);
} }
private ISkinSource currentSkin; private ISkinSource currentSkin;
@ -186,7 +188,7 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h); public override bool Remove(DrawableHitObject h) => Columns.ElementAt(((ManiaHitObject)h.HitObject).Column - firstColumnIndex).Remove(h);
public void Add(BarLine barLine) => base.Add(new DrawableBarLine(barLine)); public void Add(BarLine barLine) => base.Add(barLine);
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{ {

View File

@ -139,7 +139,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
Duration = taikoDuration, Duration = taikoDuration,
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
}; };
} }

View File

@ -3,7 +3,6 @@
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Threading; using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
@ -14,7 +13,7 @@ using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
{ {
public class DrumRoll : TaikoStrongableHitObject, IHasPath, IHasSliderVelocity public class DrumRoll : TaikoStrongableHitObject, IHasPath
{ {
/// <summary> /// <summary>
/// Drum roll distance that results in a duration of 1 speed-adjusted beat length. /// Drum roll distance that results in a duration of 1 speed-adjusted beat length.
@ -34,19 +33,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary> /// </summary>
public double Velocity { get; private set; } public double Velocity { get; private set; }
public BindableNumber<double> SliderVelocityBindable { get; } = new BindableDouble(1)
{
Precision = 0.01,
MinValue = 0.1,
MaxValue = 10
};
public double SliderVelocity
{
get => SliderVelocityBindable.Value;
set => SliderVelocityBindable.Value = value;
}
/// <summary> /// <summary>
/// Numer of ticks per beat length. /// Numer of ticks per beat length.
/// </summary> /// </summary>
@ -63,8 +49,9 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime);
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity; double scoringDistance = base_distance * difficulty.SliderMultiplier * effectPoint.ScrollSpeed;
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
TickRate = difficulty.SliderTickRate == 3 ? 3 : 4; TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;

View File

@ -92,25 +92,6 @@ namespace osu.Game.Tests.Visual.Editing
} }
[Test] [Test]
[FlakyTest]
/*
* Fail rate around 1.2%.
*
* Failing with realm refetch occasionally being null.
* My only guess is that the WorkingBeatmap at SetupScreen is dummy instead of the true one.
* If it's something else, we have larger issues with realm, but I don't think that's the case.
*
* at osu.Framework.Logging.ThrowingTraceListener.Fail(String message1, String message2)
* at System.Diagnostics.TraceInternal.Fail(String message, String detailMessage)
* at System.Diagnostics.TraceInternal.TraceProvider.Fail(String message, String detailMessage)
* at System.Diagnostics.Debug.Fail(String message, String detailMessage)
* at osu.Game.Database.ModelManager`1.<>c__DisplayClass8_0.<performFileOperation>b__0(Realm realm) ModelManager.cs:line 50
* at osu.Game.Database.RealmExtensions.Write(Realm realm, Action`1 function) RealmExtensions.cs:line 14
* at osu.Game.Database.ModelManager`1.performFileOperation(TModel item, Action`1 operation) ModelManager.cs:line 47
* at osu.Game.Database.ModelManager`1.AddFile(TModel item, Stream contents, String filename) ModelManager.cs:line 37
* at osu.Game.Screens.Edit.Setup.ResourcesSection.ChangeAudioTrack(FileInfo source) ResourcesSection.cs:line 115
* at osu.Game.Tests.Visual.Editing.TestSceneEditorBeatmapCreation.<TestAddAudioTrack>b__11_0() TestSceneEditorBeatmapCreation.cs:line 101
*/
public void TestAddAudioTrack() public void TestAddAudioTrack()
{ {
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual); AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);

View File

@ -185,6 +185,34 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10); AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
} }
[Test]
public void TestGetSegmentEnds()
{
var positions = new[]
{
Vector2.Zero,
new Vector2(100, 0),
new Vector2(100),
new Vector2(200, 100),
};
double[] distances = { 100d, 200d, 300d };
AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear))));
AddAssert("segment ends are correct", () => path.GetSegmentEnds().SequenceEqual(distances.Select(d => d / 300)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)).SequenceEqual(positions.Skip(1)));
AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 400);
AddAssert("segment ends are correct", () => path.GetSegmentEnds().SequenceEqual(distances.Select(d => d / 400)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)).SequenceEqual(positions.Skip(1)));
AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
AddAssert("segment ends are correct", () => path.GetSegmentEnds().SequenceEqual(distances.Select(d => d / 150)));
AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)).SequenceEqual(new[]
{
positions[1],
new Vector2(100, 50),
new Vector2(100, 50),
}));
}
private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints) private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints)
{ {
var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList(); var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();

View File

@ -84,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
public void TestFocusOnTabKeyWhenExpanded() public void TestFocusOnEnterKeyWhenExpanded()
{ {
setLocalUserPlaying(true); setLocalUserPlaying(true);
assertChatFocused(false); assertChatFocused(false);
AddStep("press tab", () => InputManager.Key(Key.Tab)); AddStep("press enter", () => InputManager.Key(Key.Enter));
assertChatFocused(true); assertChatFocused(true);
} }
@ -99,19 +99,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
setLocalUserPlaying(true); setLocalUserPlaying(true);
assertChatFocused(false); assertChatFocused(false);
AddStep("press tab", () => InputManager.Key(Key.Tab)); AddStep("press enter", () => InputManager.Key(Key.Enter));
assertChatFocused(true); assertChatFocused(true);
AddStep("press escape", () => InputManager.Key(Key.Escape)); AddStep("press escape", () => InputManager.Key(Key.Escape));
assertChatFocused(false); assertChatFocused(false);
} }
[Test] [Test]
public void TestFocusOnTabKeyWhenNotExpanded() public void TestFocusOnEnterKeyWhenNotExpanded()
{ {
AddStep("set not expanded", () => chatDisplay.Expanded.Value = false); AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
AddUntilStep("is not visible", () => !chatDisplay.IsPresent); AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
AddStep("press tab", () => InputManager.Key(Key.Tab)); AddStep("press enter", () => InputManager.Key(Key.Enter));
assertChatFocused(true); assertChatFocused(true);
AddUntilStep("is visible", () => chatDisplay.IsPresent); AddUntilStep("is visible", () => chatDisplay.IsPresent);
@ -120,21 +120,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("is not visible", () => !chatDisplay.IsPresent); AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
} }
[Test]
public void TestFocusToggleViaAction()
{
AddStep("set not expanded", () => chatDisplay.Expanded.Value = false);
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
AddStep("press tab", () => InputManager.Key(Key.Tab));
assertChatFocused(true);
AddUntilStep("is visible", () => chatDisplay.IsPresent);
AddStep("press tab", () => InputManager.Key(Key.Tab));
assertChatFocused(false);
AddUntilStep("is not visible", () => !chatDisplay.IsPresent);
}
private void assertChatFocused(bool isFocused) => private void assertChatFocused(bool isFocused) =>
AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused); AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused);

View File

@ -49,6 +49,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
{ {
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Expanded = { Value = true } Expanded = { Value = true }
}, Add); }, Add);
}); });

View File

@ -79,6 +79,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 20); AddWaitStep("wait a bit", 20);
} }
[TestCase(2)]
[TestCase(16)]
public void TestTeams(int count)
{
int[] userIds = getPlayerIds(count);
start(userIds, teams: true);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
}
[Test] [Test]
public void TestMultipleStartRequests() public void TestMultipleStartRequests()
{ {
@ -450,16 +463,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null) private void start(int[] userIds, int? beatmapId = null, APIMod[]? mods = null, bool teams = false)
{ {
AddStep("start play", () => AddStep("start play", () =>
{ {
foreach (int id in userIds) for (int i = 0; i < userIds.Length; i++)
{ {
int id = userIds[i];
var user = new MultiplayerRoomUser(id) var user = new MultiplayerRoomUser(id)
{ {
User = new APIUser { Id = id }, User = new APIUser { Id = id },
Mods = mods ?? Array.Empty<APIMod>(), Mods = mods ?? Array.Empty<APIMod>(),
MatchState = teams ? new TeamVersusUserState { TeamID = i % 2 } : null,
}; };
OnlinePlayDependencies.MultiplayerClient.AddUser(user, true); OnlinePlayDependencies.MultiplayerClient.AddUser(user, true);

View File

@ -0,0 +1,130 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSliderWithTextBoxInput : OsuManualInputManagerTestScene
{
private SliderWithTextBoxInput<float> sliderWithTextBoxInput = null!;
private OsuSliderBar<float> slider => sliderWithTextBoxInput.ChildrenOfType<OsuSliderBar<float>>().Single();
private Nub nub => sliderWithTextBoxInput.ChildrenOfType<Nub>().Single();
private OsuTextBox textBox => sliderWithTextBoxInput.ChildrenOfType<OsuTextBox>().Single();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create slider", () => Child = sliderWithTextBoxInput = new SliderWithTextBoxInput<float>("Test Slider")
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Current = new BindableFloat
{
MinValue = -5,
MaxValue = 5,
Precision = 0.2f
}
});
}
[Test]
public void TestNonInstantaneousMode()
{
AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero);
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("3"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
}
[Test]
public void TestInstantaneousMode()
{
AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
}
}
}

View File

@ -4,6 +4,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
@ -17,11 +18,11 @@ namespace osu.Game.Tournament.Tests.Components
[Cached] [Cached]
private readonly LadderInfo ladder = new LadderInfo(); private readonly LadderInfo ladder = new LadderInfo();
[Test] private SongBar songBar = null!;
public void TestSongBar()
{
SongBar songBar = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create bar", () => Child = songBar = new SongBar AddStep("create bar", () => Child = songBar = new SongBar
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -29,7 +30,11 @@ namespace osu.Game.Tournament.Tests.Components
Origin = Anchor.Centre Origin = Anchor.Centre
}); });
AddUntilStep("wait for loaded", () => songBar.IsLoaded); AddUntilStep("wait for loaded", () => songBar.IsLoaded);
}
[Test]
public void TestSongBar()
{
AddStep("set beatmap", () => AddStep("set beatmap", () =>
{ {
var beatmap = CreateAPIBeatmap(Ruleset.Value); var beatmap = CreateAPIBeatmap(Ruleset.Value);

View File

@ -14,7 +14,6 @@ using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Tournament.Models;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -22,14 +21,14 @@ namespace osu.Game.Tournament.Components
{ {
public partial class SongBar : CompositeDrawable public partial class SongBar : CompositeDrawable
{ {
private TournamentBeatmap? beatmap; private IBeatmapInfo? beatmap;
public const float HEIGHT = 145 / 2f; public const float HEIGHT = 145 / 2f;
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!; private IBindable<RulesetInfo> ruleset { get; set; } = null!;
public TournamentBeatmap? Beatmap public IBeatmapInfo? Beatmap
{ {
set set
{ {
@ -37,7 +36,7 @@ namespace osu.Game.Tournament.Components
return; return;
beatmap = value; beatmap = value;
update(); refreshContent();
} }
} }
@ -49,7 +48,7 @@ namespace osu.Game.Tournament.Components
set set
{ {
mods = value; mods = value;
update(); refreshContent();
} }
} }
@ -71,19 +70,25 @@ namespace osu.Game.Tournament.Components
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuColour colours)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Box
{
Colour = colours.Gray3,
RelativeSizeAxes = Axes.Both,
},
flow = new FillFlowContainer flow = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
LayoutDuration = 500,
LayoutEasing = Easing.OutQuint,
Direction = FillDirection.Full, Direction = FillDirection.Full,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
@ -93,7 +98,7 @@ namespace osu.Game.Tournament.Components
Expanded = true; Expanded = true;
} }
private void update() private void refreshContent()
{ {
if (beatmap == null) if (beatmap == null)
{ {
@ -229,7 +234,7 @@ namespace osu.Game.Tournament.Components
} }
} }
}, },
new TournamentBeatmapPanel(beatmap) new UnmaskedTournamentBeatmapPanel(beatmap)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Width = 0.5f, Width = 0.5f,
@ -272,4 +277,18 @@ namespace osu.Game.Tournament.Components
} }
} }
} }
internal partial class UnmaskedTournamentBeatmapPanel : TournamentBeatmapPanel
{
public UnmaskedTournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "")
: base(beatmap, mod)
{
}
[BackgroundDependencyLoader]
private void load()
{
Masking = false;
}
}
} }

View File

@ -20,7 +20,7 @@ namespace osu.Game.Tournament.Components
{ {
public partial class TournamentBeatmapPanel : CompositeDrawable public partial class TournamentBeatmapPanel : CompositeDrawable
{ {
public readonly TournamentBeatmap? Beatmap; public readonly IBeatmapInfo? Beatmap;
private readonly string mod; private readonly string mod;
@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Components
private Box flash = null!; private Box flash = null!;
public TournamentBeatmapPanel(TournamentBeatmap? beatmap, string mod = "") public TournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "")
{ {
Beatmap = beatmap; Beatmap = beatmap;
this.mod = mod; this.mod = mod;
@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Components
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.5f), Colour = OsuColour.Gray(0.5f),
OnlineInfo = Beatmap, OnlineInfo = (Beatmap as IBeatmapSetOnlineInfo),
}, },
new FillFlowContainer new FillFlowContainer
{ {

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tournament.IPC
public Bindable<LegacyMods> Mods { get; } = new Bindable<LegacyMods>(); public Bindable<LegacyMods> Mods { get; } = new Bindable<LegacyMods>();
public Bindable<TourneyState> State { get; } = new Bindable<TourneyState>(); public Bindable<TourneyState> State { get; } = new Bindable<TourneyState>();
public Bindable<string> ChatChannel { get; } = new Bindable<string>(); public Bindable<string> ChatChannel { get; } = new Bindable<string>();
public BindableInt Score1 { get; } = new BindableInt(); public BindableLong Score1 { get; } = new BindableLong();
public BindableInt Score2 { get; } = new BindableInt(); public BindableLong Score2 { get; } = new BindableLong();
} }
} }

View File

@ -1,181 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Game.Screens.Play.HUD;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.IPC; using osu.Game.Tournament.IPC;
using osuTK;
namespace osu.Game.Tournament.Screens.Gameplay.Components namespace osu.Game.Tournament.Screens.Gameplay.Components
{ {
// TODO: Update to derive from osu-side class? public partial class TournamentMatchScoreDisplay : MatchScoreDisplay
public partial class TournamentMatchScoreDisplay : CompositeDrawable
{ {
private const float bar_height = 18;
private readonly BindableInt score1 = new BindableInt();
private readonly BindableInt score2 = new BindableInt();
private readonly MatchScoreCounter score1Text;
private readonly MatchScoreCounter score2Text;
private readonly MatchScoreDiffCounter scoreDiffText;
private readonly Drawable score1Bar;
private readonly Drawable score2Bar;
public TournamentMatchScoreDisplay()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new[]
{
new Box
{
Name = "top bar red (static)",
RelativeSizeAxes = Axes.X,
Height = bar_height / 4,
Width = 0.5f,
Colour = TournamentGame.COLOUR_RED,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight
},
new Box
{
Name = "top bar blue (static)",
RelativeSizeAxes = Axes.X,
Height = bar_height / 4,
Width = 0.5f,
Colour = TournamentGame.COLOUR_BLUE,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft
},
score1Bar = new Box
{
Name = "top bar red",
RelativeSizeAxes = Axes.X,
Height = bar_height,
Width = 0,
Colour = TournamentGame.COLOUR_RED,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight
},
score1Text = new MatchScoreCounter
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
score2Bar = new Box
{
Name = "top bar blue",
RelativeSizeAxes = Axes.X,
Height = bar_height,
Width = 0,
Colour = TournamentGame.COLOUR_BLUE,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft
},
score2Text = new MatchScoreCounter
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
scoreDiffText = new MatchScoreDiffCounter
{
Anchor = Anchor.TopCentre,
Margin = new MarginPadding
{
Top = bar_height / 4,
Horizontal = 8
},
Alpha = 0
}
};
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(MatchIPCInfo ipc) private void load(MatchIPCInfo ipc)
{ {
score1.BindValueChanged(_ => updateScores()); Team1Score.BindTo(ipc.Score1);
score1.BindTo(ipc.Score1); Team2Score.BindTo(ipc.Score2);
score2.BindValueChanged(_ => updateScores());
score2.BindTo(ipc.Score2);
}
private void updateScores()
{
score1Text.Current.Value = score1.Value;
score2Text.Current.Value = score2.Value;
var winningText = score1.Value > score2.Value ? score1Text : score2Text;
var losingText = score1.Value <= score2.Value ? score1Text : score2Text;
winningText.Winning = true;
losingText.Winning = false;
var winningBar = score1.Value > score2.Value ? score1Bar : score2Bar;
var losingBar = score1.Value <= score2.Value ? score1Bar : score2Bar;
int diff = Math.Max(score1.Value, score2.Value) - Math.Min(score1.Value, score2.Value);
losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint);
scoreDiffText.Alpha = diff != 0 ? 1 : 0;
scoreDiffText.Current.Value = -diff;
scoreDiffText.Origin = score1.Value > score2.Value ? Anchor.TopLeft : Anchor.TopRight;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
score1Text.X = -Math.Max(5 + score1Text.DrawWidth / 2, score1Bar.DrawWidth);
score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
private partial class MatchScoreCounter : CommaSeparatedScoreCounter
{
private OsuSpriteText displayedSpriteText = null!;
public MatchScoreCounter()
{
Margin = new MarginPadding { Top = bar_height, Horizontal = 10 };
}
public bool Winning
{
set => updateFont(value);
}
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
{
displayedSpriteText = s;
displayedSpriteText.Spacing = new Vector2(-6);
updateFont(false);
});
private void updateFont(bool winning)
=> displayedSpriteText.Font = winning
? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true);
}
private partial class MatchScoreDiffCounter : CommaSeparatedScoreCounter
{
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
{
s.Spacing = new Vector2(-2);
s.Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: bar_height, fixedWidth: true);
});
} }
} }
} }

View File

@ -12,25 +12,26 @@ using osu.Game.Skinning;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
/// <summary> /// <summary>
/// A <see cref="WorkingBeatmap"/> which can be constructed directly from a .osu file, providing an implementation for /// A <see cref="WorkingBeatmap"/> which can be constructed directly from an .osu file (via <see cref="FlatWorkingBeatmap(string, int?)"/>)
/// or an <see cref="IBeatmap"/> instance (via <see cref="FlatWorkingBeatmap(IBeatmap)"/>,
/// providing an implementation for
/// <see cref="WorkingBeatmap.GetPlayableBeatmap(osu.Game.Rulesets.IRulesetInfo,System.Collections.Generic.IReadOnlyList{osu.Game.Rulesets.Mods.Mod})"/>. /// <see cref="WorkingBeatmap.GetPlayableBeatmap(osu.Game.Rulesets.IRulesetInfo,System.Collections.Generic.IReadOnlyList{osu.Game.Rulesets.Mods.Mod})"/>.
/// </summary> /// </summary>
public class FlatFileWorkingBeatmap : WorkingBeatmap public class FlatWorkingBeatmap : WorkingBeatmap
{ {
private readonly Beatmap beatmap; private readonly IBeatmap beatmap;
public FlatFileWorkingBeatmap(string file, int? beatmapId = null) public FlatWorkingBeatmap(string file, int? beatmapId = null)
: this(readFromFile(file), beatmapId) : this(readFromFile(file))
{ {
if (beatmapId.HasValue)
beatmap.BeatmapInfo.OnlineID = beatmapId.Value;
} }
private FlatFileWorkingBeatmap(Beatmap beatmap, int? beatmapId = null) public FlatWorkingBeatmap(IBeatmap beatmap)
: base(beatmap.BeatmapInfo, null) : base(beatmap.BeatmapInfo, null)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
if (beatmapId.HasValue)
beatmap.BeatmapInfo.OnlineID = beatmapId.Value;
} }
private static Beatmap readFromFile(string filename) private static Beatmap readFromFile(string filename)

View File

@ -237,6 +237,12 @@ namespace osu.Game.Configuration
value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(), value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(),
shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons)) shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))
), ),
new TrackedSetting<bool>(OsuSetting.GameplayLeaderboard, state => new SettingDescription(
rawValue: state,
name: GlobalActionKeyBindingStrings.ToggleInGameLeaderboard,
value: state ? CommonStrings.Enabled.ToLower() : CommonStrings.Disabled.ToLower(),
shortcut: LookupKeyBindings(GlobalAction.ToggleInGameLeaderboard))
),
new TrackedSetting<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode, visibilityMode => new SettingDescription( new TrackedSetting<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode, visibilityMode => new SettingDescription(
rawValue: visibilityMode, rawValue: visibilityMode,
name: GameplaySettingsStrings.HUDVisibilityMode, name: GameplaySettingsStrings.HUDVisibilityMode,

View File

@ -29,9 +29,9 @@ namespace osu.Game.Database
protected override Stream? GetFileContents(BeatmapSetInfo model, INamedFileUsage file) protected override Stream? GetFileContents(BeatmapSetInfo model, INamedFileUsage file)
{ {
bool isBeatmap = model.Beatmaps.Any(o => o.Hash == file.File.Hash); var beatmapInfo = model.Beatmaps.SingleOrDefault(o => o.Hash == file.File.Hash);
if (!isBeatmap) if (beatmapInfo == null)
return base.GetFileContents(model, file); return base.GetFileContents(model, file);
// Read the beatmap contents and skin // Read the beatmap contents and skin
@ -43,6 +43,9 @@ namespace osu.Game.Database
using var contentStreamReader = new LineBufferedReader(contentStream); using var contentStreamReader = new LineBufferedReader(contentStream);
var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader); var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader);
var workingBeatmap = new FlatWorkingBeatmap(beatmapContent);
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset);
using var skinStream = base.GetFileContents(model, file); using var skinStream = base.GetFileContents(model, file);
if (skinStream == null) if (skinStream == null)
@ -56,10 +59,10 @@ namespace osu.Game.Database
// Convert beatmap elements to be compatible with legacy format // Convert beatmap elements to be compatible with legacy format
// So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves // So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves
foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints) foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints)
controlPoint.Time = Math.Floor(controlPoint.Time); controlPoint.Time = Math.Floor(controlPoint.Time);
foreach (var hitObject in beatmapContent.HitObjects) foreach (var hitObject in playableBeatmap.HitObjects)
{ {
// Truncate end time before truncating start time because end time is dependent on start time // Truncate end time before truncating start time because end time is dependent on start time
if (hitObject is IHasDuration hasDuration && hitObject is not IHasPath) if (hitObject is IHasDuration hasDuration && hitObject is not IHasPath)
@ -86,7 +89,7 @@ namespace osu.Game.Database
// Encode to legacy format // Encode to legacy format
var stream = new MemoryStream(); var stream = new MemoryStream();
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw);
stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin);

View File

@ -52,7 +52,7 @@ namespace osu.Game.Database
// (ie. if an async import finished very recently). // (ie. if an async import finished very recently).
Realm.Realm.Write(realm => Realm.Realm.Write(realm =>
{ {
var managed = realm.Find<TModel>(item.ID); var managed = realm.FindWithRefresh<TModel>(item.ID);
Debug.Assert(managed != null); Debug.Assert(managed != null);
operation(managed); operation(managed);

View File

@ -82,8 +82,9 @@ namespace osu.Game.Database
/// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations. /// 30 2023-06-16 Run migration of old lazer scores again. This time with more correct rounding considerations.
/// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores. /// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores.
/// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files.
/// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding.
/// </summary> /// </summary>
private const int schema_version = 32; private const int schema_version = 33;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -771,6 +772,7 @@ namespace osu.Game.Database
break; break;
case 8: case 8:
{
// Ctrl -/+ now adjusts UI scale so let's clear any bindings which overlap these combinations. // Ctrl -/+ now adjusts UI scale so let's clear any bindings which overlap these combinations.
// New defaults will be populated by the key store afterwards. // New defaults will be populated by the key store afterwards.
var keyBindings = migration.NewRealm.All<RealmKeyBinding>(); var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
@ -784,6 +786,7 @@ namespace osu.Game.Database
migration.NewRealm.Remove(decreaseSpeedBinding); migration.NewRealm.Remove(decreaseSpeedBinding);
break; break;
}
case 9: case 9:
// Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well. // Pretty pointless to do this as beatmaps aren't really loaded via realm yet, but oh well.
@ -838,6 +841,7 @@ namespace osu.Game.Database
break; break;
case 11: case 11:
{
string keyBindingClassName = getMappedOrOriginalName(typeof(RealmKeyBinding)); string keyBindingClassName = getMappedOrOriginalName(typeof(RealmKeyBinding));
if (!migration.OldRealm.Schema.TryFindObjectSchema(keyBindingClassName, out _)) if (!migration.OldRealm.Schema.TryFindObjectSchema(keyBindingClassName, out _))
@ -864,6 +868,7 @@ namespace osu.Game.Database
} }
break; break;
}
case 14: case 14:
foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>()) foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>())
@ -1012,6 +1017,19 @@ namespace osu.Game.Database
break; break;
} }
case 33:
{
// Clear default bindings for the chat focus toggle,
// as they would conflict with the newly-added leaderboard toggle.
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
var toggleChatBind = keyBindings.FirstOrDefault(bind => bind.ActionInt == (int)GlobalAction.ToggleChatFocus);
if (toggleChatBind != null && toggleChatBind.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Tab }))
migration.NewRealm.Remove(toggleChatBind);
break;
}
} }
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");

View File

@ -8,6 +8,34 @@ namespace osu.Game.Database
{ {
public static class RealmExtensions public static class RealmExtensions
{ {
/// <summary>
/// Performs a <see cref="Realm.Find{T}(System.Nullable{long})"/>.
/// If a match was not found, a <see cref="Realm.Refresh"/> is performed before trying a second time.
/// This ensures that an instance is found even if the realm requested against was not in a consistent state.
/// </summary>
/// <param name="realm">The realm to operate on.</param>
/// <param name="id">The ID of the entity to find in the realm.</param>
/// <typeparam name="T">The type of the entity to find in the realm.</typeparam>
/// <returns>
/// The retrieved entity of type <typeparamref name="T"/>.
/// Can be <see langword="null"/> if the entity is still not found by <paramref name="id"/> even after a refresh.
/// </returns>
public static T? FindWithRefresh<T>(this Realm realm, Guid id) where T : IRealmObject
{
var found = realm.Find<T>(id);
if (found == null)
{
// It may be that we access this from the update thread before a refresh has taken place.
// To ensure that behaviour matches what we'd expect (the object generally *should be* available), force
// a refresh to bring in any off-thread changes immediately.
realm.Refresh();
found = realm.Find<T>(id);
}
return found;
}
/// <summary> /// <summary>
/// Perform a write operation against the provided realm instance. /// Perform a write operation against the provided realm instance.
/// </summary> /// </summary>

View File

@ -30,7 +30,7 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Construct a new instance of live realm data. /// Construct a new instance of live realm data.
/// </summary> /// </summary>
/// <param name="data">The realm data.</param> /// <param name="data">The realm data. Must be managed (see <see cref="IRealmObjectBase.IsManaged"/>).</param>
/// <param name="realm">The realm factory the data was sourced from. May be null for an unmanaged object.</param> /// <param name="realm">The realm factory the data was sourced from. May be null for an unmanaged object.</param>
public RealmLive(T data, RealmAccess realm) public RealmLive(T data, RealmAccess realm)
: base(data.ID) : base(data.ID)
@ -62,7 +62,7 @@ namespace osu.Game.Database
return; return;
} }
perform(retrieveFromID(r)); perform(r.FindWithRefresh<T>(ID)!);
RealmLiveStatistics.USAGE_ASYNC.Value++; RealmLiveStatistics.USAGE_ASYNC.Value++;
}); });
} }
@ -84,7 +84,7 @@ namespace osu.Game.Database
return realm.Run(r => return realm.Run(r =>
{ {
var returnData = perform(retrieveFromID(r)); var returnData = perform(r.FindWithRefresh<T>(ID)!);
RealmLiveStatistics.USAGE_ASYNC.Value++; RealmLiveStatistics.USAGE_ASYNC.Value++;
if (returnData is RealmObjectBase realmObject && realmObject.IsManaged) if (returnData is RealmObjectBase realmObject && realmObject.IsManaged)
@ -141,25 +141,10 @@ namespace osu.Game.Database
} }
dataIsFromUpdateThread = true; dataIsFromUpdateThread = true;
data = retrieveFromID(realm.Realm); data = realm.Realm.FindWithRefresh<T>(ID)!;
RealmLiveStatistics.USAGE_UPDATE_REFETCH.Value++; RealmLiveStatistics.USAGE_UPDATE_REFETCH.Value++;
} }
private T retrieveFromID(Realm realm)
{
var found = realm.Find<T>(ID);
if (found == null)
{
// It may be that we access this from the update thread before a refresh has taken place.
// To ensure that behaviour matches what we'd expect (the object *is* available), force
// a refresh to bring in any off-thread changes immediately.
realm.Refresh();
found = realm.Find<T>(ID)!;
}
return found;
}
} }
internal static class RealmLiveStatistics internal static class RealmLiveStatistics

View File

@ -49,6 +49,18 @@ namespace osu.Game.Graphics
this.maxDuration = maxDuration; this.maxDuration = maxDuration;
} }
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(active =>
{
// ensure that particles can be spawned immediately after the spewer becomes active.
if (active.NewValue)
lastParticleAdded = null;
});
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -56,12 +68,8 @@ namespace osu.Game.Graphics
Invalidate(Invalidation.DrawNode); Invalidate(Invalidation.DrawNode);
if (!Active.Value || !CanSpawnParticles) if (!Active.Value || !CanSpawnParticles)
{
lastParticleAdded = null;
return; return;
}
// Always want to spawn the first particle in an activation immediately.
if (lastParticleAdded == null) if (lastParticleAdded == null)
{ {
lastParticleAdded = Time.Current; lastParticleAdded = Time.Current;

View File

@ -213,7 +213,7 @@ namespace osu.Game.Graphics.UserInterface
requestDisplay(); requestDisplay();
else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000 && !IsHovered) else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000 && !IsHovered)
{ {
mainContent.FadeTo(0, 300, Easing.OutQuint); mainContent.FadeTo(0.7f, 300, Easing.OutQuint);
isDisplayed = false; isDisplayed = false;
} }
} }

View File

@ -0,0 +1,143 @@
// 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.Globalization;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
/// <summary>
/// A custom step value for each key press which actuates a change on this control.
/// </summary>
public float KeyboardStep
{
get => slider.KeyboardStep;
set => slider.KeyboardStep = value;
}
public Bindable<T> Current
{
get => slider.Current;
set => slider.Current = value;
}
private bool instantaneous;
/// <summary>
/// Whether changes to the slider should instantaneously transfer to the text box (and vice versa).
/// If <see langword="false"/>, the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end.
/// </summary>
public bool Instantaneous
{
get => instantaneous;
set
{
instantaneous = value;
slider.TransferValueOnCommit = !instantaneous;
}
}
private readonly SettingsSlider<T> slider;
private readonly LabelledTextBox textBox;
public SliderWithTextBoxInput(LocalisableString labelText)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{
textBox = new LabelledTextBox
{
Label = labelText,
},
slider = new SettingsSlider<T>
{
TransferValueOnCommit = true,
RelativeSizeAxes = Axes.X,
}
}
},
};
textBox.OnCommit += textCommitted;
textBox.Current.BindValueChanged(textChanged);
Current.BindValueChanged(updateTextBoxFromSlider, true);
}
private bool updatingFromTextBox;
private void textChanged(ValueChangedEvent<string> change)
{
if (!instantaneous) return;
tryUpdateSliderFromTextBox();
}
private void textCommitted(TextBox t, bool isNew)
{
tryUpdateSliderFromTextBox();
// If the attempted update above failed, restore text box to match the slider.
Current.TriggerChange();
}
private void tryUpdateSliderFromTextBox()
{
updatingFromTextBox = true;
try
{
switch (slider.Current)
{
case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(textBox.Current.Value);
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(textBox.Current.Value);
break;
default:
slider.Current.Parse(textBox.Current.Value);
break;
}
}
catch
{
// ignore parsing failures.
// sane state will eventually be restored by a commit (either explicit, or implicit via focus loss).
}
updatingFromTextBox = false;
}
private void updateTextBoxFromSlider(ValueChangedEvent<T> _)
{
if (updatingFromTextBox) return;
decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
}
}
}

View File

@ -116,9 +116,10 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface),
new KeyBinding(InputKey.Tab, GlobalAction.ToggleInGameLeaderboard),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), new KeyBinding(InputKey.Enter, GlobalAction.ToggleChatFocus),
new KeyBinding(InputKey.F1, GlobalAction.SaveReplay), new KeyBinding(InputKey.F1, GlobalAction.SaveReplay),
new KeyBinding(InputKey.F2, GlobalAction.ExportReplay), new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
}; };
@ -204,7 +205,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleMute))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleMute))]
ToggleMute, ToggleMute,
// In-Game Keybindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SkipCutscene))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SkipCutscene))]
SkipCutscene, SkipCutscene,
@ -232,7 +232,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.QuickExit))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.QuickExit))]
QuickExit, QuickExit,
// Game-wide beatmap music controller keybindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.MusicNext))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.MusicNext))]
MusicNext, MusicNext,
@ -260,7 +259,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PauseGameplay))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PauseGameplay))]
PauseGameplay, PauseGameplay,
// Editor
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSetupMode))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSetupMode))]
EditorSetupMode, EditorSetupMode,
@ -285,7 +283,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameInterface))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameInterface))]
ToggleInGameInterface, ToggleInGameInterface,
// Song select keybindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleModSelection))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleModSelection))]
ToggleModSelection, ToggleModSelection,
@ -378,5 +375,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleReplaySettings))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleReplaySettings))]
ToggleReplaySettings, ToggleReplaySettings,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))]
ToggleInGameLeaderboard,
} }
} }

View File

@ -219,6 +219,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ToggleInGameInterface => new TranslatableString(getKey(@"toggle_in_game_interface"), @"Toggle in-game interface"); public static LocalisableString ToggleInGameInterface => new TranslatableString(getKey(@"toggle_in_game_interface"), @"Toggle in-game interface");
/// <summary>
/// "Toggle in-game leaderboard"
/// </summary>
public static LocalisableString ToggleInGameLeaderboard => new TranslatableString(getKey(@"toggle_in_game_leaderboard"), @"Toggle in-game leaderboard");
/// <summary> /// <summary>
/// "Toggle mod select" /// "Toggle mod select"
/// </summary> /// </summary>

View File

@ -20,7 +20,7 @@ namespace osu.Game.Online.API
public Bindable<APIUser> LocalUser { get; } = new Bindable<APIUser>(new APIUser public Bindable<APIUser> LocalUser { get; } = new Bindable<APIUser>(new APIUser
{ {
Username = @"Dummy", Username = @"Local user",
Id = DUMMY_USER_ID, Id = DUMMY_USER_ID,
}); });

View File

@ -104,9 +104,11 @@ namespace osu.Game.Rulesets.Difficulty
public virtual void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo) public virtual void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
{ {
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO]; MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
LegacyAccuracyScore = (int)values[ATTRIB_ID_LEGACY_ACCURACY_SCORE];
LegacyComboScore = (int)values[ATTRIB_ID_LEGACY_COMBO_SCORE]; // Temporarily allow these attributes to not exist so as to not block releases of server-side components while these attributes aren't populated/used yet.
LegacyBonusScoreRatio = (int)values[ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO]; LegacyAccuracyScore = (int)values.GetValueOrDefault(ATTRIB_ID_LEGACY_ACCURACY_SCORE);
LegacyComboScore = (int)values.GetValueOrDefault(ATTRIB_ID_LEGACY_COMBO_SCORE);
LegacyBonusScoreRatio = values.GetValueOrDefault(ATTRIB_ID_LEGACY_BONUS_SCORE_RATIO);
} }
} }
} }

View File

@ -40,11 +40,13 @@ namespace osu.Game.Rulesets.Objects
private readonly List<Vector2> calculatedPath = new List<Vector2>(); private readonly List<Vector2> calculatedPath = new List<Vector2>();
private readonly List<double> cumulativeLength = new List<double>(); private readonly List<double> cumulativeLength = new List<double>();
private readonly List<int> segmentEnds = new List<int>();
private readonly Cached pathCache = new Cached(); private readonly Cached pathCache = new Cached();
private double calculatedLength; private double calculatedLength;
private readonly List<int> segmentEnds = new List<int>();
private double[] segmentEndDistances = Array.Empty<double>();
/// <summary> /// <summary>
/// Creates a new <see cref="SliderPath"/>. /// Creates a new <see cref="SliderPath"/>.
/// </summary> /// </summary>
@ -202,7 +204,7 @@ namespace osu.Game.Rulesets.Objects
{ {
ensureValid(); ensureValid();
return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength); return segmentEndDistances.Select(d => d / Distance);
} }
private void invalidate() private void invalidate()
@ -251,8 +253,9 @@ namespace osu.Game.Rulesets.Objects
calculatedPath.Add(t); calculatedPath.Add(t);
} }
// Remember the index of the segment end if (i > 0)
segmentEnds.Add(calculatedPath.Count - 1); // Remember the index of the segment end
segmentEnds.Add(calculatedPath.Count - 1);
// Start the new segment at the current vertex // Start the new segment at the current vertex
start = i; start = i;
@ -298,6 +301,14 @@ namespace osu.Game.Rulesets.Objects
cumulativeLength.Add(calculatedLength); cumulativeLength.Add(calculatedLength);
} }
// Store the distances of the segment ends now, because after shortening the indices may be out of range
segmentEndDistances = new double[segmentEnds.Count];
for (int i = 0; i < segmentEnds.Count; i++)
{
segmentEndDistances[i] = cumulativeLength[segmentEnds[i]];
}
if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance) if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
{ {
// In osu-stable, if the last two control points of a slider are equal, extension is not performed. // In osu-stable, if the last two control points of a slider are equal, extension is not performed.
@ -319,10 +330,6 @@ namespace osu.Game.Rulesets.Objects
{ {
cumulativeLength.RemoveAt(cumulativeLength.Count - 1); cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
calculatedPath.RemoveAt(pathEndIndex--); calculatedPath.RemoveAt(pathEndIndex--);
// Shorten the last segment to the expected distance
if (segmentEnds.Count > 0)
segmentEnds[^1]--;
} }
} }

View File

@ -199,6 +199,8 @@ namespace osu.Game.Screens.Edit
if (loadableBeatmap is DummyWorkingBeatmap) if (loadableBeatmap is DummyWorkingBeatmap)
{ {
Logger.Log("Editor was loaded without a valid beatmap; creating a new beatmap.");
isNewBeatmap = true; isNewBeatmap = true;
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);

View File

@ -147,13 +147,25 @@ namespace osu.Game.Screens.Edit.Timing
trackedType = null; trackedType = null;
else else
{ {
// If the selected group only has one control point, update the tracking type. switch (selectedGroup.Value.ControlPoints.Count)
if (selectedGroup.Value.ControlPoints.Count == 1) {
trackedType = selectedGroup.Value?.ControlPoints.Single().GetType(); // If the selected group has no control points, clear the tracked type.
// If the selected group has more than one control point, choose the first as the tracking type // Otherwise the user will be unable to select a group with no control points.
// if we don't already have a singular tracked type. case 0:
else if (trackedType == null) trackedType = null;
trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType(); break;
// If the selected group only has one control point, update the tracking type.
case 1:
trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
break;
// If the selected group has more than one control point, choose the first as the tracking type
// if we don't already have a singular tracked type.
default:
trackedType ??= selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
break;
}
} }
if (trackedType != null) if (trackedType != null)

View File

@ -1,106 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
private readonly SettingsSlider<T> slider;
public SliderWithTextBoxInput(LocalisableString labelText)
{
LabelledTextBox textBox;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{
textBox = new LabelledTextBox
{
Label = labelText,
},
slider = new SettingsSlider<T>
{
TransferValueOnCommit = true,
RelativeSizeAxes = Axes.X,
}
}
},
};
textBox.OnCommit += (t, isNew) =>
{
if (!isNew) return;
try
{
switch (slider.Current)
{
case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(t.Text);
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(t.Text);
break;
default:
slider.Current.Parse(t.Text);
break;
}
}
catch
{
// TriggerChange below will restore the previous text value on failure.
}
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
Current.TriggerChange();
};
Current.BindValueChanged(_ =>
{
decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
}, true);
}
/// <summary>
/// A custom step value for each key press which actuates a change on this control.
/// </summary>
public float KeyboardStep
{
get => slider.KeyboardStep;
set => slider.KeyboardStep = value;
}
public Bindable<T> Current
{
get => slider.Current;
set => slider.Current = value;
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Logging;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -59,6 +60,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public bool Seek(double position) public bool Seek(double position)
{ {
Logger.Log($"{nameof(SpectatorPlayerClock)} seeked to {position}");
CurrentTime = position; CurrentTime = position;
return true; return true;
} }

View File

@ -160,6 +160,21 @@ namespace osu.Game.Screens.Play
Seek(StartTime); Seek(StartTime);
// This is a workaround for the fact that DecoupleableInterpolatingFramedClock doesn't seek the source
// if the source is not IsRunning. (see https://github.com/ppy/osu-framework/blob/2102638056dfcf85d21b4d85266d53b5dd018767/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs#L209-L210)
// I hope to remove this once we knock some sense into clocks in general.
//
// Without this seek, the multiplayer spectator start sequence breaks:
// - Individual clients' clocks are never updated to their expected time
// - The sync manager thinks they are running behind
// - Gameplay doesn't start when it should (until a timeout occurs because nothing is happening for 10+ seconds)
//
// In addition, we use `CurrentTime` for this seek instead of `StartTime` as the above seek may have applied inherent
// offsets which need to be accounted for (ie. FramedBeatmapClock.TotalAppliedOffset).
//
// See https://github.com/ppy/osu/pull/24451/files/87fee001c786b29db34063ef3350e9a9f024d3ab#diff-28ca02979641e2d98a15fe5d5e806f56acf60ac100258a059fa72503b6cc54e8.
(SourceClock as IAdjustableClock)?.Seek(CurrentTime);
if (!wasPaused || startClock) if (!wasPaused || startClock)
Start(); Start();
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -24,11 +22,13 @@ namespace osu.Game.Screens.Play.HUD
public BindableLong Team1Score = new BindableLong(); public BindableLong Team1Score = new BindableLong();
public BindableLong Team2Score = new BindableLong(); public BindableLong Team2Score = new BindableLong();
protected MatchScoreCounter Score1Text; protected MatchScoreCounter Score1Text = null!;
protected MatchScoreCounter Score2Text; protected MatchScoreCounter Score2Text = null!;
private Drawable score1Bar; private Drawable score1Bar = null!;
private Drawable score2Bar; private Drawable score2Bar = null!;
private MatchScoreDiffCounter scoreDiffText = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
@ -98,6 +98,16 @@ namespace osu.Game.Screens.Play.HUD
}, },
} }
}, },
scoreDiffText = new MatchScoreDiffCounter
{
Anchor = Anchor.TopCentre,
Margin = new MarginPadding
{
Top = bar_height / 4,
Horizontal = 8
},
Alpha = 0
}
}; };
} }
@ -139,6 +149,10 @@ namespace osu.Game.Screens.Play.HUD
losingBar.ResizeWidthTo(0, 400, Easing.OutQuint); losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint); winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint);
scoreDiffText.Alpha = diff != 0 ? 1 : 0;
scoreDiffText.Current.Value = -diff;
scoreDiffText.Origin = Team1Score.Value > Team2Score.Value ? Anchor.TopLeft : Anchor.TopRight;
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
@ -150,7 +164,7 @@ namespace osu.Game.Screens.Play.HUD
protected partial class MatchScoreCounter : CommaSeparatedScoreCounter protected partial class MatchScoreCounter : CommaSeparatedScoreCounter
{ {
private OsuSpriteText displayedSpriteText; private OsuSpriteText displayedSpriteText = null!;
public MatchScoreCounter() public MatchScoreCounter()
{ {
@ -174,5 +188,14 @@ namespace osu.Game.Screens.Play.HUD
? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true) ? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true); : OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true);
} }
private partial class MatchScoreDiffCounter : CommaSeparatedScoreCounter
{
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
{
s.Spacing = new Vector2(-2);
s.Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: bar_height, fixedWidth: true);
});
}
} }
} }

View File

@ -81,6 +81,7 @@ namespace osu.Game.Screens.Play
public Bindable<bool> ShowHud { get; } = new BindableBool(); public Bindable<bool> ShowHud { get; } = new BindableBool();
private Bindable<HUDVisibilityMode> configVisibilityMode; private Bindable<HUDVisibilityMode> configVisibilityMode;
private Bindable<bool> configLeaderboardVisibility;
private Bindable<bool> configSettingsOverlay; private Bindable<bool> configSettingsOverlay;
private readonly BindableBool replayLoaded = new BindableBool(); private readonly BindableBool replayLoaded = new BindableBool();
@ -186,6 +187,7 @@ namespace osu.Game.Screens.Play
ModDisplay.Current.Value = mods; ModDisplay.Current.Value = mods;
configVisibilityMode = config.GetBindable<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode); configVisibilityMode = config.GetBindable<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode);
configLeaderboardVisibility = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard);
configSettingsOverlay = config.GetBindable<bool>(OsuSetting.ReplaySettingsOverlay); configSettingsOverlay = config.GetBindable<bool>(OsuSetting.ReplaySettingsOverlay);
if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce) if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce)
@ -398,6 +400,10 @@ namespace osu.Game.Screens.Play
} }
return true; return true;
case GlobalAction.ToggleInGameLeaderboard:
configLeaderboardVisibility.Value = !configLeaderboardVisibility.Value;
return true;
} }
return false; return false;

View File

@ -810,10 +810,13 @@ namespace osu.Game.Screens.Play
if (!canShowResults && !forceImport) if (!canShowResults && !forceImport)
return Task.FromResult<ScoreInfo>(null); return Task.FromResult<ScoreInfo>(null);
// Clone score before beginning any async processing.
// - Must be run synchronously as the score may potentially be mutated in the background.
// - Must be cloned for the same reason.
Score scoreCopy = Score.DeepClone();
return prepareScoreForDisplayTask = Task.Run(async () => return prepareScoreForDisplayTask = Task.Run(async () =>
{ {
var scoreCopy = Score.DeepClone();
try try
{ {
await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false); await PrepareScoreForResultsAsync(scoreCopy).ConfigureAwait(false);

View File

@ -124,7 +124,12 @@ namespace osu.Game.Tests.Visual.Spectator
if (frames.Count == 0) if (frames.Count == 0)
return; return;
var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray()); var bundle = new FrameDataBundle(new ScoreInfo
{
Combo = currentFrameIndex,
TotalScore = (long)(currentFrameIndex * 123478 * RNG.NextDouble(0.99, 1.01)),
Accuracy = RNG.NextDouble(0.98, 1),
}, new ScoreProcessor(rulesetStore.GetRuleset(0)!.CreateInstance()), frames.ToArray());
((ISpectatorClient)this).UserSentFrames(userId, bundle); ((ISpectatorClient)this).UserSentFrames(userId, bundle);
frames.Clear(); frames.Clear();

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.1.2" /> <PackageReference Include="Realm" Version="11.1.2" />
<PackageReference Include="ppy.osu.Framework" Version="2023.801.0" /> <PackageReference Include="ppy.osu.Framework" Version="2023.815.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.719.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2023.719.0" />
<PackageReference Include="Sentry" Version="3.28.1" /> <PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.801.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2023.815.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>