1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 19:42:55 +08:00

Merge branch 'master' into judge-fix

This commit is contained in:
Bartłomiej Dach 2023-08-22 09:44:58 +02:00 committed by GitHub
commit 8cd9f0822a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 1670 additions and 688 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.811.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.817.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

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
<!-- for editor usage -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
</manifest> </manifest>

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

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- using a different name because package name cannot contain 'catch' --> <!-- using a different name because package name cannot contain 'catch' -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Catch_Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Catch_Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!catch Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!catch Test" />
</manifest> </manifest>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Mania.Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Mania.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!mania Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!mania Test" />
</manifest> </manifest>

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

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Osu.Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Osu.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!standard Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!standard Test" />
</manifest> </manifest>

View File

@ -9,6 +9,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
@ -70,12 +71,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
base.Content.Children = new Drawable[] base.Content.Children = new Drawable[]
{ {
editorClock = new EditorClock(editorBeatmap), editorClock = new EditorClock(editorBeatmap),
snapProvider, new PopoverContainer { Child = snapProvider },
Content Content
}; };
} }
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>

View File

@ -0,0 +1,95 @@
// 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.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestScenePreciseRotation : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset);
[Test]
public void TestHotkeyHandling()
{
AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
AddStep("select first three objects", () =>
{
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Take(3));
});
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.EqualTo(1));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
}
[Test]
public void TestRotateCorrectness()
{
AddStep("replace objects", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.AddRange(new HitObject[]
{
new HitCircle { Position = new Vector2(100) },
new HitCircle { Position = new Vector2(200) },
});
});
AddStep("select both circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
AddStep("rotate by 180deg", () => getPopover().ChildrenOfType<TextBox>().Single().Current.Value = "180");
AddAssert("first object rotated 180deg around playfield centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position,
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(100)));
AddAssert("second object rotated 180deg around playfield centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(1).TriggerClick());
AddAssert("first object rotated 90deg around selection centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
AddAssert("second object rotated 90deg around selection centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position, () => Is.EqualTo(new Vector2(100, 100)));
PreciseRotationPopover? getPopover() => this.ChildrenOfType<PreciseRotationPopover>().SingleOrDefault();
}
}
}

View File

@ -0,0 +1,110 @@
// 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.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestSceneSliderReversal : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
private readonly PathControlPoint[][] paths =
{
createPathSegment(
PathType.PerfectCurve,
new Vector2(200, -50),
new Vector2(250, 0)
),
createPathSegment(
PathType.Linear,
new Vector2(100, 0),
new Vector2(100, 100)
)
};
private static PathControlPoint[] createPathSegment(PathType type, params Vector2[] positions)
{
return positions.Select(p => new PathControlPoint
{
Position = p
}).Prepend(new PathControlPoint
{
Type = type
}).ToArray();
}
private Slider selectedSlider => (Slider)EditorBeatmap.SelectedHitObjects[0];
[TestCase(0, 250)]
[TestCase(0, 200)]
[TestCase(1, 120)]
[TestCase(1, 80)]
public void TestSliderReversal(int pathIndex, double length)
{
var controlPoints = paths[pathIndex];
Vector2 oldStartPos = default;
Vector2 oldEndPos = default;
double oldDistance = default;
var oldControlPointTypes = controlPoints.Select(p => p.Type);
AddStep("Add slider", () =>
{
var slider = new Slider
{
Position = new Vector2(OsuPlayfield.BASE_SIZE.X / 2, OsuPlayfield.BASE_SIZE.Y / 2),
Path = new SliderPath(controlPoints)
{
ExpectedDistance = { Value = length }
}
};
EditorBeatmap.Add(slider);
oldStartPos = slider.Position;
oldEndPos = slider.EndPosition;
oldDistance = slider.Path.Distance;
});
AddStep("Select slider", () =>
{
var slider = (Slider)EditorBeatmap.HitObjects[0];
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddStep("Reverse slider", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.G);
InputManager.ReleaseKey(Key.LControl);
});
AddAssert("Slider has correct length", () =>
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
AddAssert("Slider has correct start position", () =>
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
AddAssert("Slider has correct end position", () =>
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
AddAssert("Control points have correct types", () =>
{
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
});
}
}
}

View File

@ -85,6 +85,11 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active // we may be entering the screen with a selection already active
updateDistanceSnapGrid(); updateDistanceSnapGrid();
RightToolbox.Add(new TransformToolboxGroup
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler
});
} }
protected override ComposeBlueprintContainer CreateBlueprintContainer() protected override ComposeBlueprintContainer CreateBlueprintContainer()

View File

@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PreciseRotationPopover : OsuPopover
{
private readonly SelectionRotationHandler rotationHandler;
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
private SliderWithTextBoxInput<float> angleInput = null!;
private EditorRadioButtonCollection rotationOrigin = null!;
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
{
this.rotationHandler = rotationHandler;
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Children = new Drawable[]
{
angleInput = new SliderWithTextBoxInput<float>("Angle (degrees):")
{
Current = new BindableNumber<float>
{
MinValue = -360,
MaxValue = 360,
Precision = 1
},
Instantaneous = true
},
rotationOrigin = new EditorRadioButtonCollection
{
RelativeSizeAxes = Axes.X,
Items = new[]
{
new RadioButton("Playfield centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
new RadioButton("Selection centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => angleInput.TakeFocus());
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
rotationOrigin.Items.First().Select();
rotationInfo.BindValueChanged(rotation =>
{
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
});
}
protected override void PopIn()
{
base.PopIn();
rotationHandler.Begin();
}
protected override void PopOut()
{
base.PopOut();
if (IsLoaded)
rotationHandler.Commit();
}
}
public enum RotationOrigin
{
PlayfieldCentre,
SelectionCentre
}
public record PreciseRotationInfo(float Degrees, RotationOrigin Origin);
}

View File

@ -0,0 +1,80 @@
// 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.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
{
private readonly Bindable<bool> canRotate = new BindableBool();
private EditorToolButton rotateButton = null!;
public SelectionRotationHandler RotationHandler { get; init; } = null!;
public TransformToolboxGroup()
: base("transform")
{
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Children = new Drawable[]
{
rotateButton = new EditorToolButton("Rotate",
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
() => new PreciseRotationPopover(RotationHandler)),
// TODO: scale
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
// bindings to `Enabled` on the buttons are decoupled on purpose
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
canRotate.BindTo(RotationHandler.CanRotate);
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat) return false;
switch (e.Action)
{
case GlobalAction.EditorToggleRotateControl:
{
rotateButton.TriggerClick();
return true;
}
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}

View File

@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
texture.Bind(); texture.Bind();
for (int i = 0; i < points.Count; i++) for (int i = 0; i < points.Count; i++)
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex); drawPointQuad(renderer, points[i], textureRect, i + firstVisiblePointIndex);
UnbindTextureShader(renderer); UnbindTextureShader(renderer);
renderer.PopLocalMatrix(); renderer.PopLocalMatrix();
@ -325,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1); private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index) private void drawPointQuad(IRenderer renderer, SmokePoint point, RectangleF textureRect, int index)
{ {
Debug.Assert(quadBatch != null); Debug.Assert(quadBatch != null);
@ -347,25 +347,25 @@ namespace osu.Game.Rulesets.Osu.Skinning
var localBotLeft = point.Position + ortho - dir; var localBotLeft = point.Position + ortho - dir;
var localBotRight = point.Position + ortho + dir; var localBotRight = point.Position + ortho + dir;
quadBatch.Add(new TexturedVertex2D quadBatch.Add(new TexturedVertex2D(renderer)
{ {
Position = localTopLeft, Position = localTopLeft,
TexturePosition = textureRect.TopLeft, TexturePosition = textureRect.TopLeft,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour), Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
}); });
quadBatch.Add(new TexturedVertex2D quadBatch.Add(new TexturedVertex2D(renderer)
{ {
Position = localTopRight, Position = localTopRight,
TexturePosition = textureRect.TopRight, TexturePosition = textureRect.TopRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour), Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
}); });
quadBatch.Add(new TexturedVertex2D quadBatch.Add(new TexturedVertex2D(renderer)
{ {
Position = localBotRight, Position = localBotRight,
TexturePosition = textureRect.BottomRight, TexturePosition = textureRect.BottomRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour), Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
}); });
quadBatch.Add(new TexturedVertex2D quadBatch.Add(new TexturedVertex2D(renderer)
{ {
Position = localBotLeft, Position = localBotLeft,
TexturePosition = textureRect.BottomLeft, TexturePosition = textureRect.BottomLeft,

View File

@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (time - part.Time >= 1) if (time - part.Time >= 1)
continue; continue;
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex(renderer)
{ {
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)), Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft, TexturePosition = textureRect.BottomLeft,
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time Time = part.Time
}); });
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex(renderer)
{ {
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)), Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight, TexturePosition = textureRect.BottomRight,
@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time Time = part.Time
}); });
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex(renderer)
{ {
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y), Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopRight, TexturePosition = textureRect.TopRight,
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time Time = part.Time
}); });
vertexBatch.Add(new TexturedTrailVertex vertexBatch.Add(new TexturedTrailVertex(renderer)
{ {
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y), Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopLeft, TexturePosition = textureRect.TopLeft,
@ -362,12 +362,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[VertexMember(1, VertexAttribPointerType.Float)] [VertexMember(1, VertexAttribPointerType.Float)]
public float Time; public float Time;
[VertexMember(1, VertexAttribPointerType.Int)]
private readonly int maskingIndex;
public TexturedTrailVertex(IRenderer renderer)
{
this = default;
maskingIndex = renderer.CurrentMaskingIndex;
}
public bool Equals(TexturedTrailVertex other) public bool Equals(TexturedTrailVertex other)
{ {
return Position.Equals(other.Position) return Position.Equals(other.Position)
&& TexturePosition.Equals(other.TexturePosition) && TexturePosition.Equals(other.TexturePosition)
&& Colour.Equals(other.Colour) && Colour.Equals(other.Colour)
&& Time.Equals(other.Time); && Time.Equals(other.Time)
&& maskingIndex == other.maskingIndex;
} }
} }
} }

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Taiko.Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Taiko.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!taiko Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!taiko Test" />
</manifest> </manifest>

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

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Tests.Android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!visual Test" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!visual Test" />
</manifest> </manifest>

View File

@ -8,6 +8,9 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -15,7 +18,7 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Database namespace osu.Game.Tests.Database
{ {
[HeadlessTest] [HeadlessTest]
public partial class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo
{ {
public IBindable<bool> IsPlaying => isPlaying; public IBindable<bool> IsPlaying => isPlaying;
@ -59,7 +62,7 @@ namespace osu.Game.Tests.Database
AddStep("Run background processor", () => AddStep("Run background processor", () =>
{ {
Add(new TestBackgroundBeatmapProcessor()); Add(new TestBackgroundDataStoreProcessor());
}); });
AddUntilStep("wait for difficulties repopulated", () => AddUntilStep("wait for difficulties repopulated", () =>
@ -98,7 +101,7 @@ namespace osu.Game.Tests.Database
AddStep("Run background processor", () => AddStep("Run background processor", () =>
{ {
Add(new TestBackgroundBeatmapProcessor()); Add(new TestBackgroundDataStoreProcessor());
}); });
AddWaitStep("wait some", 500); AddWaitStep("wait some", 500);
@ -124,7 +127,58 @@ namespace osu.Game.Tests.Database
}); });
} }
public partial class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor [Test]
public void TestScoreUpgradeSuccess()
{
ScoreInfo scoreInfo = null!;
AddStep("Add score which requires upgrade (and has beatmap)", () =>
{
Realm.Write(r =>
{
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
{
TotalScoreVersion = 30000002,
LegacyTotalScore = 123456,
IsLegacyScore = true,
});
});
});
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
}
[Test]
public void TestScoreUpgradeFailed()
{
ScoreInfo scoreInfo = null!;
AddStep("Add score which requires upgrade (but has no beatmap)", () =>
{
Realm.Write(r =>
{
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo(),
Ruleset = r.All<RulesetInfo>().First(),
})
{
TotalScoreVersion = 30000002,
IsLegacyScore = true,
});
});
});
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
}
public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
{ {
protected override int TimeToSleepDuringGameplay => 10; protected override int TimeToSleepDuringGameplay => 10;
} }

View File

@ -1,19 +1,23 @@
// 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 System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Database namespace osu.Game.Tests.Database
{ {
[TestFixture] [TestFixture]
public class LegacyBeatmapImporterTest public class LegacyBeatmapImporterTest : RealmTest
{ {
private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter(); private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter();
@ -60,6 +64,33 @@ namespace osu.Game.Tests.Database
} }
} }
[Test]
public void TestStableDateAddedApplied()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
{
var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);
ZipFile.ExtractToDirectory(TestResources.GetQuickTestBeatmapForImport(), songsStorage.GetFullPath("renatus"));
string[] beatmaps = Directory.GetFiles(songsStorage.GetFullPath("renatus"), "*.osu", SearchOption.TopDirectoryOnly);
File.SetLastWriteTimeUtc(beatmaps[beatmaps.Length / 2], new DateTime(2000, 1, 1, 12, 0, 0));
await new LegacyBeatmapImporter(new BeatmapImporter(storage, realm)).ImportFromStableAsync(stableStorage);
var importedSet = realm.Realm.All<BeatmapSetInfo>().Single();
Assert.NotNull(importedSet);
Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Utc)), importedSet.DateAdded);
}
});
}
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
{ {
public TestLegacyBeatmapImporter() public TestLegacyBeatmapImporter()

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -29,7 +30,7 @@ namespace osu.Game.Tests.Editing
[Cached(typeof(IBeatSnapProvider))] [Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap; private readonly EditorBeatmap editorBeatmap;
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
public TestSceneHitObjectComposerDistanceSnapping() public TestSceneHitObjectComposerDistanceSnapping()
{ {

View File

@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
void main(void) void main(void)
{ {
// Transform from screen space to masking space. // Transform from screen space to masking space.
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0); highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z; v_MaskingPosition = maskingPos.xy / maskingPos.z;
v_Colour = m_Colour; v_Colour = m_Colour;

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

@ -8,7 +8,7 @@ using System.Linq;
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.Graphics.Containers; using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5); AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
} }
public partial class EditorBeatmapContainer : Container public partial class EditorBeatmapContainer : PopoverContainer
{ {
private readonly IWorkingBeatmap working; private readonly IWorkingBeatmap working;

View File

@ -185,6 +185,37 @@ 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(), () => Is.EqualTo(distances.Select(d => d / 300)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 400);
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 400)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 150)));
// see remarks in `GetSegmentEnds()` xmldoc (`SliderPath.PositionAt()` clamps progress to [0,1]).
AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(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

@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Menus
foreach (var fountain in Children.OfType<StarFountain>()) foreach (var fountain in Children.OfType<StarFountain>())
{ {
if (RNG.NextSingle() > 0.8f) if (RNG.NextSingle() > 0.8f)
fountain.Shoot(); fountain.Shoot(RNG.Next(-1, 2));
} }
}, 150); }, 150);
} }

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

@ -2,25 +2,35 @@
// 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 NUnit.Framework; using NUnit.Framework;
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.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Tests.Components namespace osu.Game.Tournament.Tests.Components
{ {
[TestFixture] [TestFixture]
public partial class TestSceneSongBar : OsuTestScene public partial class TestSceneSongBar : TournamentTestScene
{ {
[Cached] private SongBar songBar = null!;
private readonly LadderInfo ladder = new LadderInfo(); private TournamentBeatmap ladderBeatmap = null!;
[Test] [SetUpSteps]
public void TestSongBar() public override void SetUpSteps()
{ {
SongBar songBar = null!; base.SetUpSteps();
AddStep("setup picks bans", () =>
{
ladderBeatmap = CreateSampleBeatmap();
Ladder.CurrentMatch.Value!.PicksBans.Add(new BeatmapChoice
{
BeatmapID = ladderBeatmap.OnlineID,
Team = TeamColour.Red,
Type = ChoiceType.Pick,
});
});
AddStep("create bar", () => Child = songBar = new SongBar AddStep("create bar", () => Child = songBar = new SongBar
{ {
@ -29,16 +39,22 @@ 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);
beatmap.CircleSize = 3.4f; beatmap.CircleSize = 3.4f;
beatmap.ApproachRate = 6.8f; beatmap.ApproachRate = 6.8f;
beatmap.OverallDifficulty = 5.5f; beatmap.OverallDifficulty = 5.5f;
beatmap.StarRating = 4.56f; beatmap.StarRating = 4.56f;
beatmap.Length = 123456; beatmap.Length = 123456;
beatmap.BPM = 133; beatmap.BPM = 133;
beatmap.OnlineID = ladderBeatmap.OnlineID;
songBar.Beatmap = new TournamentBeatmap(beatmap); songBar.Beatmap = new TournamentBeatmap(beatmap);
}); });

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)
{ {

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

@ -51,7 +51,7 @@ namespace osu.Game.Tournament.Models
public Bindable<int> LastYearPlacing = new BindableInt public Bindable<int> LastYearPlacing = new BindableInt
{ {
MinValue = 1, MinValue = 0,
MaxValue = 256 MaxValue = 256
}; };

View File

@ -10,7 +10,9 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
@ -128,7 +130,7 @@ namespace osu.Game.Tournament.Screens.Editors
Width = 0.2f, Width = 0.2f,
Current = Model.Seed Current = Model.Seed
}, },
new SettingsSlider<int> new SettingsSlider<int, LastYearPlacementSlider>
{ {
LabelText = "Last Year Placement", LabelText = "Last Year Placement",
Width = 0.33f, Width = 0.33f,
@ -175,6 +177,11 @@ namespace osu.Game.Tournament.Screens.Editors
}; };
} }
private partial class LastYearPlacementSlider : RoundedSliderBar<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText;
}
public partial class PlayerEditor : CompositeDrawable public partial class PlayerEditor : CompositeDrawable
{ {
private readonly TournamentTeam team; private readonly TournamentTeam team;

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

@ -274,7 +274,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } }, new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } },
new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"), new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"),
new RowDisplay("Seed:", team.Seed.Value), new RowDisplay("Seed:", team.Seed.Value),
new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "0"), new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "N/A"),
new Container { Margin = new MarginPadding { Bottom = 30 } }, new Container { Margin = new MarginPadding { Bottom = 30 } },
} }
}, },

View File

@ -24,7 +24,10 @@ using osu.Game.Screens.Play;
namespace osu.Game namespace osu.Game
{ {
public partial class BackgroundBeatmapProcessor : Component /// <summary>
/// Performs background updating of data stores at startup.
/// </summary>
public partial class BackgroundDataStoreProcessor : Component
{ {
[Resolved] [Resolved]
private RulesetStore rulesetStore { get; set; } = null!; private RulesetStore rulesetStore { get; set; } = null!;
@ -61,7 +64,8 @@ namespace osu.Game
Task.Factory.StartNew(() => Task.Factory.StartNew(() =>
{ {
Logger.Log("Beginning background beatmap processing.."); Logger.Log("Beginning background data store processing..");
checkForOutdatedStarRatings(); checkForOutdatedStarRatings();
processBeatmapSetsWithMissingMetrics(); processBeatmapSetsWithMissingMetrics();
processScoresWithMissingStatistics(); processScoresWithMissingStatistics();
@ -74,7 +78,7 @@ namespace osu.Game
return; return;
} }
Logger.Log("Finished background beatmap processing!"); Logger.Log("Finished background data store processing!");
}); });
} }
@ -182,7 +186,7 @@ namespace osu.Game
realmAccess.Run(r => realmAccess.Run(r =>
{ {
foreach (var score in r.All<ScoreInfo>()) foreach (var score in r.All<ScoreInfo>().Where(s => !s.BackgroundReprocessingFailed))
{ {
if (score.BeatmapInfo != null if (score.BeatmapInfo != null
&& score.Statistics.Sum(kvp => kvp.Value) > 0 && score.Statistics.Sum(kvp => kvp.Value) > 0
@ -221,6 +225,7 @@ namespace osu.Game
catch (Exception e) catch (Exception e)
{ {
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}"); Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
} }
} }
} }
@ -230,7 +235,7 @@ namespace osu.Game
Logger.Log("Querying for scores that need total score conversion..."); Logger.Log("Querying for scores that need total score conversion...");
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>() HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
.Where(s => s.BeatmapInfo != null && s.TotalScoreVersion == 30000002) .Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
.AsEnumerable().Select(s => s.ID))); .AsEnumerable().Select(s => s.ID)));
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion."); Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
@ -279,6 +284,7 @@ namespace osu.Game
catch (Exception e) catch (Exception e)
{ {
Logger.Log($"Failed to convert total score for {id}: {e}"); Logger.Log($"Failed to convert total score for {id}: {e}");
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
++failedCount; ++failedCount;
} }
} }

View File

@ -152,6 +152,8 @@ namespace osu.Game.Beatmaps
if (archive != null) if (archive != null)
beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm)); beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm));
beatmapSet.DateAdded = getDateAdded(archive);
foreach (BeatmapInfo b in beatmapSet.Beatmaps) foreach (BeatmapInfo b in beatmapSet.Beatmaps)
{ {
b.BeatmapSet = beatmapSet; b.BeatmapSet = beatmapSet;
@ -305,11 +307,36 @@ namespace osu.Game.Beatmaps
return new BeatmapSetInfo return new BeatmapSetInfo
{ {
OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1, OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1,
// Metadata = beatmap.Metadata,
DateAdded = DateTimeOffset.UtcNow
}; };
} }
/// <summary>
/// Determine the date a given beatmapset has been added to the game.
/// For legacy imports, we can use the oldest file write time for any `.osu` file in the directory.
/// For any other import types, use "now".
/// </summary>
private DateTimeOffset getDateAdded(ArchiveReader? reader)
{
DateTimeOffset dateAdded = DateTimeOffset.UtcNow;
if (reader is LegacyDirectoryArchiveReader legacyReader)
{
var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
dateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmaps.First()));
foreach (string beatmapName in beatmaps)
{
var currentDateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmapName));
if (currentDateAdded < dateAdded)
dateAdded = currentDateAdded;
}
}
return dateAdded;
}
/// <summary> /// <summary>
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive. /// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
/// </summary> /// </summary>

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

@ -1,13 +1,10 @@
// 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 System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -45,7 +42,7 @@ namespace osu.Game.Beatmaps.Formats
/// Register dependencies for use with static decoder classes. /// Register dependencies for use with static decoder classes.
/// </summary> /// </summary>
/// <param name="rulesets">A store containing all available rulesets (used by <see cref="LegacyBeatmapDecoder"/>).</param> /// <param name="rulesets">A store containing all available rulesets (used by <see cref="LegacyBeatmapDecoder"/>).</param>
public static void RegisterDependencies([NotNull] RulesetStore rulesets) public static void RegisterDependencies(RulesetStore rulesets)
{ {
LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets)); LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets));
} }
@ -63,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats
throw new IOException(@"Unknown decoder type"); throw new IOException(@"Unknown decoder type");
// start off with the first line of the file // start off with the first line of the file
string line = stream.PeekLine()?.Trim(); string? line = stream.PeekLine()?.Trim();
while (line != null && line.Length == 0) while (line != null && line.Length == 0)
{ {

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.Collections.Generic; using System.Collections.Generic;
using osuTK.Graphics; using osuTK.Graphics;
@ -13,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats
/// <summary> /// <summary>
/// Retrieves the list of combo colours for presentation only. /// Retrieves the list of combo colours for presentation only.
/// </summary> /// </summary>
IReadOnlyList<Color4> ComboColours { get; } IReadOnlyList<Color4>? ComboColours { get; }
/// <summary> /// <summary>
/// The list of custom combo colours. /// The list of custom combo colours.

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
#pragma warning disable 618 #pragma warning disable 618
using System; using System;
@ -36,11 +34,11 @@ namespace osu.Game.Beatmaps.Formats
/// </summary> /// </summary>
private const double control_point_leniency = 1; private const double control_point_leniency = 1;
internal static RulesetStore RulesetStore; internal static RulesetStore? RulesetStore;
private Beatmap beatmap; private Beatmap beatmap = null!;
private ConvertHitObjectParser parser; private ConvertHitObjectParser? parser;
private LegacySampleBank defaultSampleBank; private LegacySampleBank defaultSampleBank;
private int defaultSampleVolume = 100; private int defaultSampleVolume = 100;
@ -222,7 +220,7 @@ namespace osu.Game.Beatmaps.Formats
case @"Mode": case @"Mode":
int rulesetID = Parsing.ParseInt(pair.Value); int rulesetID = Parsing.ParseInt(pair.Value);
beatmap.BeatmapInfo.Ruleset = RulesetStore.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally."); beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
switch (rulesetID) switch (rulesetID)
{ {

View File

@ -1,15 +1,12 @@
// 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 System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using JetBrains.Annotations;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
@ -34,8 +31,7 @@ namespace osu.Game.Beatmaps.Formats
private readonly IBeatmap beatmap; private readonly IBeatmap beatmap;
[CanBeNull] private readonly ISkin? skin;
private readonly ISkin skin;
private readonly int onlineRulesetID; private readonly int onlineRulesetID;
@ -44,7 +40,7 @@ namespace osu.Game.Beatmaps.Formats
/// </summary> /// </summary>
/// <param name="beatmap">The beatmap to encode.</param> /// <param name="beatmap">The beatmap to encode.</param>
/// <param name="skin">The beatmap's skin, used for encoding combo colours.</param> /// <param name="skin">The beatmap's skin, used for encoding combo colours.</param>
public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin) public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
this.skin = skin; this.skin = skin;
@ -180,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[TimingPoints]"); writer.WriteLine("[TimingPoints]");
SampleControlPoint lastRelevantSamplePoint = null; SampleControlPoint? lastRelevantSamplePoint = null;
DifficultyControlPoint lastRelevantDifficultyPoint = null; DifficultyControlPoint? lastRelevantDifficultyPoint = null;
// In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats. // In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats.
// In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored. // In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored.
@ -585,7 +581,7 @@ namespace osu.Game.Beatmaps.Formats
return type; return type;
} }
private LegacySampleBank toLegacySampleBank(string sampleBank) private LegacySampleBank toLegacySampleBank(string? sampleBank)
{ {
switch (sampleBank?.ToLowerInvariant()) switch (sampleBank?.ToLowerInvariant())
{ {
@ -603,7 +599,7 @@ namespace osu.Game.Beatmaps.Formats
} }
} }
private int toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) private int toLegacyCustomSampleBank(HitSampleInfo? hitSampleInfo)
{ {
if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy) if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
return legacy.CustomSampleBank; return legacy.CustomSampleBank;

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 System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -19,10 +17,10 @@ namespace osu.Game.Beatmaps.Formats
{ {
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard> public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
{ {
private StoryboardSprite storyboardSprite; private StoryboardSprite? storyboardSprite;
private CommandTimelineGroup timelineGroup; private CommandTimelineGroup? timelineGroup;
private Storyboard storyboard; private Storyboard storyboard = null!;
private readonly Dictionary<string, string> variables = new Dictionary<string, string>(); private readonly Dictionary<string, string> variables = new Dictionary<string, string>();

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)
@ -67,7 +70,22 @@ namespace osu.Game.Database
hitObject.StartTime = Math.Floor(hitObject.StartTime); hitObject.StartTime = Math.Floor(hitObject.StartTime);
if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue; if (hitObject is not IHasPath hasPath) continue;
// stable's hit object parsing expects the entire slider to use only one type of curve,
// and happens to use the last non-empty curve type read for the entire slider.
// this clear of the last control point type handles an edge case
// wherein the last control point of an otherwise-single-segment slider path has a different type than previous,
// which would lead to sliders being mangled when exported back to stable.
// normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below,
// which outputs a slider path containing only Bezier control points,
// but a non-inherited last control point is (rightly) not considered to be starting a new segment,
// therefore it would fail to clear the `CountSegments() <= 1` check.
// by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier.
if (hasPath.Path.ControlPoints.Count > 1)
hasPath.Path.ControlPoints[^1].Type = null;
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue;
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints); var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
@ -86,7 +104,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,10 @@ 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.
/// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
/// </summary> /// </summary>
private const int schema_version = 32; private const int schema_version = 34;
/// <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 +773,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 +787,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 +842,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 +869,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 +1018,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

@ -40,8 +40,14 @@ namespace osu.Game.Graphics.UserInterface
AddInternal(hoverClickSounds = new HoverClickSounds()); AddInternal(hoverClickSounds = new HoverClickSounds());
updateTextColour(); updateTextColour();
}
protected override void LoadComplete()
{
base.LoadComplete();
Item.Action.BindDisabledChanged(_ => updateState(), true); Item.Action.BindDisabledChanged(_ => updateState(), true);
FinishTransforms();
} }
private void updateTextColour() private void updateTextColour()

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

@ -35,6 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public string Text public string Text
{ {
get => Component.Text;
set => Component.Text = value; set => Component.Text = value;
} }

View File

@ -0,0 +1,145 @@
// 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);
}
public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox);
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

@ -21,7 +21,9 @@ namespace osu.Game.IO.Archives
this.path = Path.GetFullPath(path); this.path = Path.GetFullPath(path);
} }
public override Stream GetStream(string name) => File.OpenRead(Path.Combine(path, name)); public override Stream GetStream(string name) => File.OpenRead(GetFullPath(name));
public string GetFullPath(string filename) => Path.Combine(path, filename);
public override void Dispose() public override void Dispose()
{ {

View File

@ -3,33 +3,26 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Localisation; using osu.Game.Localisation;
namespace osu.Game.Input.Bindings namespace osu.Game.Input.Bindings
{ {
public partial class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput public partial class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput, IKeyBindingHandler<GlobalAction>
{ {
private readonly Drawable? handler; private readonly IKeyBindingHandler<GlobalAction>? handler;
private InputManager? parentInputManager;
public GlobalActionContainer(OsuGameBase? game) public GlobalActionContainer(OsuGameBase? game)
: base(matchingMode: KeyCombinationMatchingMode.Modifiers) : base(matchingMode: KeyCombinationMatchingMode.Modifiers)
{ {
if (game is IKeyBindingHandler<GlobalAction>) if (game is IKeyBindingHandler<GlobalAction> h)
handler = game; handler = h;
} }
protected override void LoadComplete() protected override bool Prioritised => true;
{
base.LoadComplete();
parentInputManager = GetContainingInputManager();
}
// IMPORTANT: Take care when changing order of the items in the enumerable. // IMPORTANT: Take care when changing order of the items in the enumerable.
// It is used to decide the order of precedence, with the earlier items having higher precedence. // It is used to decide the order of precedence, with the earlier items having higher precedence.
@ -105,6 +98,7 @@ namespace osu.Game.Input.Bindings
// See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38. // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38.
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
}; };
public IEnumerable<KeyBinding> InGameKeyBindings => new[] public IEnumerable<KeyBinding> InGameKeyBindings => new[]
@ -116,9 +110,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),
}; };
@ -159,20 +154,9 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.F3, GlobalAction.MusicPlay) new KeyBinding(InputKey.F3, GlobalAction.MusicPlay)
}; };
protected override IEnumerable<Drawable> KeyBindingInputQueue public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) => handler?.OnPressed(e) == true;
{
get
{
// To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content.
// It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly
// allow the whole game to handle these actions.
// An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging. public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) => handler?.OnReleased(e);
var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue;
return handler != null ? inputQueue.Prepend(handler) : inputQueue;
}
}
} }
public enum GlobalAction public enum GlobalAction
@ -204,7 +188,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 +215,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 +242,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 +266,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 +358,11 @@ 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,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
EditorToggleRotateControl,
} }
} }

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>
@ -339,6 +344,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay"); public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
/// <summary>
/// "Toggle rotate control"
/// </summary>
public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class OnlinePlayStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.OnlinePlay";
/// <summary>
/// "Playlist durations longer than 2 weeks require an active osu!supporter tag."
/// </summary>
public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

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

@ -182,8 +182,6 @@ namespace osu.Game.Online.Chat
private readonly Message message; private readonly Message message;
private readonly Channel channel; private readonly Channel channel;
public override bool IsImportant => false;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay) private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay)
{ {

View File

@ -1025,7 +1025,7 @@ namespace osu.Game
loadComponentSingleFile(CreateHighPerformanceSession(), Add); loadComponentSingleFile(CreateHighPerformanceSession(), Add);
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
Add(difficultyRecommender); Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener()); Add(externalLinkOpener = new ExternalLinkOpener());

View File

@ -392,17 +392,18 @@ namespace osu.Game
{ {
SafeAreaOverrideEdges = SafeAreaOverrideEdges, SafeAreaOverrideEdges = SafeAreaOverrideEdges,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = CreateScalingContainer().WithChildren(new Drawable[] Child = CreateScalingContainer().WithChild(globalBindings = new GlobalActionContainer(this)
{ {
(GlobalCursorDisplay = new GlobalCursorDisplay Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both (GlobalCursorDisplay = new GlobalCursorDisplay
}).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor) {
{ RelativeSizeAxes = Axes.Both
RelativeSizeAxes = Axes.Both }).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor)
}), {
// to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything. RelativeSizeAxes = Axes.Both
globalBindings = new GlobalActionContainer(this) }),
}
}) })
}); });

View File

@ -43,6 +43,9 @@ namespace osu.Game.Overlays
[Resolved] [Resolved]
private AudioManager audio { get; set; } = null!; private AudioManager audio { get; set; } = null!;
[Resolved]
private OsuGame? game { get; set; }
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
@ -176,6 +179,12 @@ namespace osu.Game.Overlays
playDebouncedSample(notification.PopInSampleName); playDebouncedSample(notification.PopInSampleName);
if (notification.IsImportant)
{
game?.Window?.Flash();
notification.Closed += () => game?.Window?.CancelFlash();
}
if (State.Value == Visibility.Hidden) if (State.Value == Visibility.Hidden)
{ {
notification.IsInToastTray = true; notification.IsInToastTray = true;

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>
@ -196,13 +198,28 @@ namespace osu.Game.Rulesets.Objects
} }
/// <summary> /// <summary>
/// Returns the progress values at which segments of the path end. /// Returns the progress values at which (control point) segments of the path end.
/// Ranges from 0 (beginning of the path) to 1 (end of the path) to infinity (beyond the end of the path).
/// </summary> /// </summary>
/// <remarks>
/// <see cref="PositionAt"/> truncates the progression values to [0,1],
/// so you can't use this method in conjunction with that one to retrieve the positions of segment ends beyond the end of the path.
/// </remarks>
/// <example>
/// <para>
/// In case <see cref="Distance"/> is less than <see cref="CalculatedDistance"/>,
/// the last segment ends after the end of the path, hence it returns a value greater than 1.
/// </para>
/// <para>
/// In case <see cref="Distance"/> is greater than <see cref="CalculatedDistance"/>,
/// the last segment ends before the end of the path, hence it returns a value less than 1.
/// </para>
/// </example>
public IEnumerable<double> GetSegmentEnds() public IEnumerable<double> GetSegmentEnds()
{ {
ensureValid(); ensureValid();
return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength); return segmentEndDistances.Select(d => d / Distance);
} }
private void invalidate() private void invalidate()
@ -251,8 +268,11 @@ 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 +318,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 +347,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

@ -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 System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -25,6 +26,53 @@ namespace osu.Game.Rulesets.Objects
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param> /// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param> /// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset) public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset)
{
var controlPoints = sliderPath.ControlPoints;
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear && p.Type is null).ToList();
// Inherited points after a linear point, as well as the first control point if it inherited,
// should be treated as linear points, so their types are temporarily changed to linear.
inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear);
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
// Remove segments after the end of the slider.
for (int numSegmentsToRemove = segmentEnds.Count(se => se >= 1) - 1; numSegmentsToRemove > 0 && controlPoints.Count > 0;)
{
if (controlPoints.Last().Type is not null)
{
numSegmentsToRemove--;
segmentEnds = segmentEnds[..^1];
}
controlPoints.RemoveAt(controlPoints.Count - 1);
}
// Restore original control point types.
inheritedLinearPoints.ForEach(p => p.Type = null);
// Recalculate middle perfect curve control points at the end of the slider path.
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && segmentEnds.Any())
{
double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
double lastSegmentEnd = segmentEnds[^1];
var circleArcPath = new List<Vector2>();
sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1);
controlPoints[^2].Position = circleArcPath[circleArcPath.Count / 2];
}
sliderPath.reverseControlPoints(out positionalOffset);
}
/// <summary>
/// Reverses the order of the provided <see cref="SliderPath"/>'s <see cref="PathControlPoint"/>s.
/// </summary>
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
private static void reverseControlPoints(this SliderPath sliderPath, out Vector2 positionalOffset)
{ {
var points = sliderPath.ControlPoints.ToArray(); var points = sliderPath.ControlPoints.ToArray();
positionalOffset = sliderPath.PositionAt(1); positionalOffset = sliderPath.PositionAt(1);

View File

@ -66,7 +66,7 @@ namespace osu.Game.Scoring
/// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>, /// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>,
/// the total score has not yet been updated to reflect the current scoring values. /// the total score has not yet been updated to reflect the current scoring values.
/// ///
/// See <see cref="BackgroundBeatmapProcessor"/>'s conversion logic. /// See <see cref="BackgroundDataStoreProcessor"/>'s conversion logic.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This may not match the version stored in the replay files. /// This may not match the version stored in the replay files.
@ -81,6 +81,15 @@ namespace osu.Game.Scoring
/// </remarks> /// </remarks>
public long? LegacyTotalScore { get; set; } public long? LegacyTotalScore { get; set; }
/// <summary>
/// If background processing of this beatmap failed in some way, this flag will become <c>true</c>.
/// Should be used to ensure we don't repeatedly attempt to reprocess the same scores each startup even though we already know they will fail.
/// </summary>
/// <remarks>
/// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk).
/// </remarks>
public bool BackgroundReprocessingFailed { get; set; }
public int MaxCombo { get; set; } public int MaxCombo { get; set; }
public double Accuracy { get; set; } public double Accuracy { get; set; }

View File

@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components
{
public partial class EditorToolButton : OsuButton, IHasPopover
{
public BindableBool Selected { get; } = new BindableBool();
private readonly Func<Drawable> createIcon;
private readonly Func<Popover?> createPopover;
private Color4 defaultBackgroundColour;
private Color4 defaultIconColour;
private Color4 selectedBackgroundColour;
private Color4 selectedIconColour;
private Drawable icon = null!;
public EditorToolButton(LocalisableString text, Func<Drawable> createIcon, Func<Popover?> createPopover)
{
Text = text;
this.createIcon = createIcon;
this.createPopover = createPopover;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
defaultBackgroundColour = colourProvider.Background3;
selectedBackgroundColour = colourProvider.Background1;
defaultIconColour = defaultBackgroundColour.Darken(0.5f);
selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Add(icon = createIcon().With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
b.Origin = Anchor.CentreLeft;
b.Size = new Vector2(20);
b.X = 10;
}));
Action = Selected.Toggle;
}
protected override void LoadComplete()
{
base.LoadComplete();
Selected.BindValueChanged(_ => updateSelectionState(), true);
}
private void updateSelectionState()
{
if (!IsLoaded)
return;
BackgroundColour = Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
icon.Colour = Selected.Value ? selectedIconColour : defaultIconColour;
if (Selected.Value)
this.ShowPopover();
else
this.HidePopover();
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 40f
};
public Popover? GetPopover() => Enabled.Value
? createPopover()?.With(p =>
{
p.State.BindValueChanged(state =>
{
if (state.NewValue == Visibility.Hidden)
Selected.Value = false;
});
})
: null;
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; } public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; }
protected SelectionHandler<T> SelectionHandler { get; private set; } public SelectionHandler<T> SelectionHandler { get; private set; }
private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>(); private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();

View File

@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
protected IEditorChangeHandler ChangeHandler { get; private set; } protected IEditorChangeHandler ChangeHandler { get; private set; }
protected SelectionRotationHandler RotationHandler { get; private set; } public SelectionRotationHandler RotationHandler { get; private set; }
protected SelectionHandler() protected SelectionHandler()
{ {

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

@ -126,12 +126,9 @@ namespace osu.Game.Screens
private void load(ShaderManager manager) private void load(ShaderManager manager)
{ {
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2_NO_MASKING, FragmentShaderDescriptor.BLUR));
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
} }

View File

@ -2,10 +2,10 @@
// 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 System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -13,6 +13,9 @@ namespace osu.Game.Screens.Menu
{ {
public partial class KiaiMenuFountains : BeatSyncedContainer public partial class KiaiMenuFountains : BeatSyncedContainer
{ {
private StarFountain leftFountain = null!;
private StarFountain rightFountain = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -20,13 +23,13 @@ namespace osu.Game.Screens.Menu
Children = new[] Children = new[]
{ {
new StarFountain leftFountain = new StarFountain
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
X = 250, X = 250,
}, },
new StarFountain rightFountain = new StarFountain
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
@ -58,8 +61,25 @@ namespace osu.Game.Screens.Menu
if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500) if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500)
return; return;
foreach (var fountain in Children.OfType<StarFountain>()) int direction = RNG.Next(-1, 2);
fountain.Shoot();
switch (direction)
{
case -1:
leftFountain.Shoot(1);
rightFountain.Shoot(-1);
break;
case 0:
leftFountain.Shoot(0);
rightFountain.Shoot(0);
break;
case 1:
leftFountain.Shoot(-1);
rightFountain.Shoot(1);
break;
}
lastTrigger = Clock.CurrentTime; lastTrigger = Clock.CurrentTime;
} }

View File

@ -23,7 +23,7 @@ namespace osu.Game.Screens.Menu
InternalChild = spewer = new StarFountainSpewer(); InternalChild = spewer = new StarFountainSpewer();
} }
public void Shoot() => spewer.Shoot(); public void Shoot(int direction) => spewer.Shoot(direction);
protected override void SkinChanged(ISkinSource skin) protected override void SkinChanged(ISkinSource skin)
{ {
@ -81,10 +81,10 @@ namespace osu.Game.Screens.Menu
return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance); return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance);
} }
public void Shoot() public void Shoot(int direction)
{ {
lastShootTime = Clock.CurrentTime; lastShootTime = Clock.CurrentTime;
lastShootDirection = RNG.Next(-1, 2); lastShootDirection = direction;
} }
private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance); private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance);

View File

@ -113,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected partial class Section : Container protected partial class Section : Container
{ {
private readonly Container content; private readonly ReverseChildIDFillFlowContainer<Drawable> content;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
@ -135,10 +135,11 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12),
Text = title.ToUpperInvariant(), Text = title.ToUpperInvariant(),
}, },
content = new Container content = new ReverseChildIDFillFlowContainer<Drawable>
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical
}, },
}, },
}; };

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

@ -23,6 +23,7 @@ using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK; using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay.Playlists namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
@ -80,6 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private IBindable<APIUser> localUser = null!; private IBindable<APIUser> localUser = null!;
private readonly Room room; private readonly Room room;
private OsuSpriteText durationNoticeText = null!;
public MatchSettings(Room room) public MatchSettings(Room room)
{ {
@ -141,14 +143,22 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}, },
new Section("Duration") new Section("Duration")
{ {
Child = new Container Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.X, new Container
Height = 40,
Child = DurationField = new DurationDropdown
{ {
RelativeSizeAxes = Axes.X RelativeSizeAxes = Axes.X,
} Height = 40,
Child = DurationField = new DurationDropdown
{
RelativeSizeAxes = Axes.X
},
},
durationNoticeText = new OsuSpriteText
{
Alpha = 0,
Colour = colours.Yellow,
},
} }
}, },
new Section("Allowed attempts (across all playlist items)") new Section("Allowed attempts (across all playlist items)")
@ -305,6 +315,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true); MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true);
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
DurationField.Current.BindValueChanged(duration =>
{
if (hasValidDuration)
durationNoticeText.Hide();
else
{
durationNoticeText.Show();
durationNoticeText.Text = OnlinePlayStrings.SupporterOnlyDurationNotice;
}
});
localUser = api.LocalUser.GetBoundCopy(); localUser = api.LocalUser.GetBoundCopy();
localUser.BindValueChanged(populateDurations, true); localUser.BindValueChanged(populateDurations, true);
@ -314,6 +335,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private void populateDurations(ValueChangedEvent<APIUser> user) private void populateDurations(ValueChangedEvent<APIUser> user)
{ {
// roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427)
// if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though.
const int days_in_month = 31;
DurationField.Items = new[] DurationField.Items = new[]
{ {
TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30),
@ -326,18 +351,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
TimeSpan.FromDays(3), TimeSpan.FromDays(3),
TimeSpan.FromDays(7), TimeSpan.FromDays(7),
TimeSpan.FromDays(14), TimeSpan.FromDays(14),
TimeSpan.FromDays(days_in_month),
TimeSpan.FromDays(days_in_month * 3),
}; };
// TODO: show these in the interface at all times.
if (user.NewValue.IsSupporter)
{
// roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427)
// if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though.
const int days_in_month = 31;
DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month));
DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month * 3));
}
} }
protected override void Update() protected override void Update()
@ -352,7 +368,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) => private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0
&& hasValidDuration;
private bool hasValidDuration => DurationField.Current.Value <= TimeSpan.FromDays(14) || localUser.Value.IsSupporter;
private void apply() private void apply()
{ {

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

@ -232,6 +232,6 @@ namespace osu.Game.Skinning
} }
private static Color4 getComboColour(IHasComboColours source, int colourIndex) private static Color4 getComboColour(IHasComboColours source, int colourIndex)
=> source.ComboColours[colourIndex % source.ComboColours.Count]; => source.ComboColours![colourIndex % source.ComboColours.Count];
} }
} }

View File

@ -203,6 +203,6 @@ namespace osu.Game.Skinning
} }
private static Color4 getComboColour(IHasComboColours source, int colourIndex) private static Color4 getComboColour(IHasComboColours source, int colourIndex)
=> source.ComboColours[colourIndex % source.ComboColours.Count]; => source.ComboColours![colourIndex % source.ComboColours.Count];
} }
} }

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