1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 03:27:24 +08:00

Merge branch 'master' into fix-export-slider

This commit is contained in:
Bartłomiej Dach 2023-08-21 07:28:16 +02:00
commit de9a4448fc
No known key found for this signature in database
57 changed files with 1004 additions and 479 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.815.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

@ -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

@ -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

@ -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,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

@ -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

@ -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

@ -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

@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

@ -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

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

View File

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

View File

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

View File

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

View File

@ -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

@ -105,6 +105,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 +117,10 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface),
new KeyBinding(InputKey.Tab, GlobalAction.ToggleInGameLeaderboard),
new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD),
new KeyBinding(InputKey.Tab, GlobalAction.ToggleChatFocus), new KeyBinding(InputKey.Enter, GlobalAction.ToggleChatFocus),
new KeyBinding(InputKey.F1, GlobalAction.SaveReplay), new KeyBinding(InputKey.F1, GlobalAction.SaveReplay),
new KeyBinding(InputKey.F2, GlobalAction.ExportReplay), new KeyBinding(InputKey.F2, GlobalAction.ExportReplay),
}; };
@ -204,7 +206,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 +233,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 +260,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 +284,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 +376,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

@ -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

@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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