mirror of
https://github.com/osukey/osukey.git
synced 2025-05-03 20:57:28 +09:00
Merge branch 'master' into master
This commit is contained in:
commit
1fd4cb8963
2
.idea/.idea.osu.Desktop/.idea/indexLayout.xml
generated
2
.idea/.idea.osu.Desktop/.idea/indexLayout.xml
generated
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ContentModelUserStore">
|
<component name="UserContentModel">
|
||||||
<attachedFolders />
|
<attachedFolders />
|
||||||
<explicitIncludes />
|
<explicitIncludes />
|
||||||
<explicitExcludes />
|
<explicitExcludes />
|
||||||
|
8
.idea/.idea.osu.Desktop/.idea/modules.xml
generated
8
.idea/.idea.osu.Desktop/.idea/modules.xml
generated
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -51,7 +51,7 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.407.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.410.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
53
osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs
Normal file
53
osu.Game.Rulesets.Catch.Tests/TestSceneCatchReplay.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
|
using osu.Game.Rulesets.Catch.UI;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Catch.Tests
|
||||||
|
{
|
||||||
|
public class TestSceneCatchReplay : TestSceneCatchPlayer
|
||||||
|
{
|
||||||
|
protected override bool Autoplay => true;
|
||||||
|
|
||||||
|
private const int object_count = 10;
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReplayCatcherPositionIsFramePerfect()
|
||||||
|
{
|
||||||
|
AddUntilStep("caught all fruits", () => Player.ScoreProcessor.Combo.Value == object_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
Ruleset = ruleset,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beatmap.ControlPointInfo.Add(0, new TimingControlPoint());
|
||||||
|
|
||||||
|
for (int i = 0; i < object_count / 2; i++)
|
||||||
|
{
|
||||||
|
beatmap.HitObjects.Add(new Fruit
|
||||||
|
{
|
||||||
|
StartTime = (i + 1) * 1000,
|
||||||
|
X = 0
|
||||||
|
});
|
||||||
|
beatmap.HitObjects.Add(new Fruit
|
||||||
|
{
|
||||||
|
StartTime = (i + 1) * 1000 + 1,
|
||||||
|
X = CatchPlayfield.WIDTH
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return beatmap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -51,8 +51,11 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
{
|
{
|
||||||
droppedObjectContainer,
|
droppedObjectContainer,
|
||||||
CatcherArea.MovableCatcher.CreateProxiedContent(),
|
CatcherArea.MovableCatcher.CreateProxiedContent(),
|
||||||
HitObjectContainer,
|
HitObjectContainer.CreateProxy(),
|
||||||
|
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
|
||||||
|
// make sure the up-to-date catcher position is used for the catcher catching logic of hit objects.
|
||||||
CatcherArea,
|
CatcherArea,
|
||||||
|
HitObjectContainer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,9 +4,11 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
@ -14,7 +16,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||||
{
|
{
|
||||||
public class TestScenePathControlPointVisualiser : OsuTestScene
|
public class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
private Slider slider;
|
private Slider slider;
|
||||||
private PathControlPointVisualiser visualiser;
|
private PathControlPointVisualiser visualiser;
|
||||||
@ -43,12 +45,145 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPerfectCurveTooManyPoints()
|
||||||
|
{
|
||||||
|
createVisualiser(true);
|
||||||
|
|
||||||
|
addControlPointStep(new Vector2(200), PathType.Bezier);
|
||||||
|
addControlPointStep(new Vector2(300));
|
||||||
|
addControlPointStep(new Vector2(500, 300));
|
||||||
|
addControlPointStep(new Vector2(700, 200));
|
||||||
|
addControlPointStep(new Vector2(500, 100));
|
||||||
|
|
||||||
|
// Must be both hovering and selecting the control point for the context menu to work.
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
|
||||||
|
addContextMenuItemStep("Perfect curve");
|
||||||
|
|
||||||
|
assertControlPointPathType(0, PathType.Bezier);
|
||||||
|
assertControlPointPathType(1, PathType.PerfectCurve);
|
||||||
|
assertControlPointPathType(3, PathType.Bezier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPerfectCurveLastThreePoints()
|
||||||
|
{
|
||||||
|
createVisualiser(true);
|
||||||
|
|
||||||
|
addControlPointStep(new Vector2(200), PathType.Bezier);
|
||||||
|
addControlPointStep(new Vector2(300));
|
||||||
|
addControlPointStep(new Vector2(500, 300));
|
||||||
|
addControlPointStep(new Vector2(700, 200));
|
||||||
|
addControlPointStep(new Vector2(500, 100));
|
||||||
|
|
||||||
|
moveMouseToControlPoint(2);
|
||||||
|
AddStep("select control point", () => visualiser.Pieces[2].IsSelected.Value = true);
|
||||||
|
addContextMenuItemStep("Perfect curve");
|
||||||
|
|
||||||
|
assertControlPointPathType(0, PathType.Bezier);
|
||||||
|
assertControlPointPathType(2, PathType.PerfectCurve);
|
||||||
|
assertControlPointPathType(4, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPerfectCurveLastTwoPoints()
|
||||||
|
{
|
||||||
|
createVisualiser(true);
|
||||||
|
|
||||||
|
addControlPointStep(new Vector2(200), PathType.Bezier);
|
||||||
|
addControlPointStep(new Vector2(300));
|
||||||
|
addControlPointStep(new Vector2(500, 300));
|
||||||
|
addControlPointStep(new Vector2(700, 200));
|
||||||
|
addControlPointStep(new Vector2(500, 100));
|
||||||
|
|
||||||
|
moveMouseToControlPoint(3);
|
||||||
|
AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
|
||||||
|
addContextMenuItemStep("Perfect curve");
|
||||||
|
|
||||||
|
assertControlPointPathType(0, PathType.Bezier);
|
||||||
|
AddAssert("point 3 is not inherited", () => slider.Path.ControlPoints[3].Type != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPerfectCurveTooManyPointsLinear()
|
||||||
|
{
|
||||||
|
createVisualiser(true);
|
||||||
|
|
||||||
|
addControlPointStep(new Vector2(200), PathType.Linear);
|
||||||
|
addControlPointStep(new Vector2(300));
|
||||||
|
addControlPointStep(new Vector2(500, 300));
|
||||||
|
addControlPointStep(new Vector2(700, 200));
|
||||||
|
addControlPointStep(new Vector2(500, 100));
|
||||||
|
|
||||||
|
// Must be both hovering and selecting the control point for the context menu to work.
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddStep("select control point", () => visualiser.Pieces[1].IsSelected.Value = true);
|
||||||
|
addContextMenuItemStep("Perfect curve");
|
||||||
|
|
||||||
|
assertControlPointPathType(0, PathType.Linear);
|
||||||
|
assertControlPointPathType(1, PathType.PerfectCurve);
|
||||||
|
assertControlPointPathType(3, PathType.Linear);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPerfectCurveChangeToBezier()
|
||||||
|
{
|
||||||
|
createVisualiser(true);
|
||||||
|
|
||||||
|
addControlPointStep(new Vector2(200), PathType.Bezier);
|
||||||
|
addControlPointStep(new Vector2(300), PathType.PerfectCurve);
|
||||||
|
addControlPointStep(new Vector2(500, 300));
|
||||||
|
addControlPointStep(new Vector2(700, 200), PathType.Bezier);
|
||||||
|
addControlPointStep(new Vector2(500, 100));
|
||||||
|
|
||||||
|
moveMouseToControlPoint(3);
|
||||||
|
AddStep("select control point", () => visualiser.Pieces[3].IsSelected.Value = true);
|
||||||
|
addContextMenuItemStep("Inherit");
|
||||||
|
|
||||||
|
assertControlPointPathType(0, PathType.Bezier);
|
||||||
|
assertControlPointPathType(1, PathType.Bezier);
|
||||||
|
assertControlPointPathType(3, null);
|
||||||
|
}
|
||||||
|
|
||||||
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection)
|
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre
|
Origin = Anchor.Centre
|
||||||
});
|
});
|
||||||
|
|
||||||
private void addControlPointStep(Vector2 position) => AddStep($"add control point {position}", () => slider.Path.ControlPoints.Add(new PathControlPoint(position)));
|
private void addControlPointStep(Vector2 position) => addControlPointStep(position, null);
|
||||||
|
|
||||||
|
private void addControlPointStep(Vector2 position, PathType? type)
|
||||||
|
{
|
||||||
|
AddStep($"add {type} control point at {position}", () =>
|
||||||
|
{
|
||||||
|
slider.Path.ControlPoints.Add(new PathControlPoint(position, type));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveMouseToControlPoint(int index)
|
||||||
|
{
|
||||||
|
AddStep($"move mouse to control point {index}", () =>
|
||||||
|
{
|
||||||
|
Vector2 position = slider.Path.ControlPoints[index].Position.Value;
|
||||||
|
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertControlPointPathType(int controlPointIndex, PathType? type)
|
||||||
|
{
|
||||||
|
AddAssert($"point {controlPointIndex} is {type}", () => slider.Path.ControlPoints[controlPointIndex].Type.Value == type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addContextMenuItemStep(string contextMenuText)
|
||||||
|
{
|
||||||
|
AddStep($"click context menu item \"{contextMenuText}\"", () =>
|
||||||
|
{
|
||||||
|
MenuItem item = visualiser.ContextMenuItems[1].Items.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
|
||||||
|
|
||||||
|
item?.Action?.Value();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,175 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||||
|
{
|
||||||
|
public class TestSceneSliderControlPointPiece : SelectionBlueprintTestScene
|
||||||
|
{
|
||||||
|
private Slider slider;
|
||||||
|
private DrawableSlider drawableObject;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
|
||||||
|
slider = new Slider
|
||||||
|
{
|
||||||
|
Position = new Vector2(256, 192),
|
||||||
|
Path = new SliderPath(new[]
|
||||||
|
{
|
||||||
|
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||||
|
new PathControlPoint(new Vector2(150, 150)),
|
||||||
|
new PathControlPoint(new Vector2(300, 0), PathType.PerfectCurve),
|
||||||
|
new PathControlPoint(new Vector2(400, 0)),
|
||||||
|
new PathControlPoint(new Vector2(400, 150))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
|
||||||
|
|
||||||
|
Add(drawableObject = new DrawableSlider(slider));
|
||||||
|
AddBlueprint(new TestSliderBlueprint(drawableObject));
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDragControlPoint()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
|
||||||
|
addMovementStep(new Vector2(150, 50));
|
||||||
|
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
assertControlPointPosition(1, new Vector2(150, 50));
|
||||||
|
assertControlPointType(0, PathType.PerfectCurve);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDragControlPointAlmostLinearlyExterior()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
|
||||||
|
addMovementStep(new Vector2(400, 0.01f));
|
||||||
|
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
assertControlPointPosition(1, new Vector2(400, 0.01f));
|
||||||
|
assertControlPointType(0, PathType.Bezier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDragControlPointPathRecovery()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(1);
|
||||||
|
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
|
||||||
|
addMovementStep(new Vector2(400, 0.01f));
|
||||||
|
assertControlPointType(0, PathType.Bezier);
|
||||||
|
|
||||||
|
addMovementStep(new Vector2(150, 50));
|
||||||
|
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
assertControlPointPosition(1, new Vector2(150, 50));
|
||||||
|
assertControlPointType(0, PathType.PerfectCurve);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDragControlPointPathRecoveryOtherSegment()
|
||||||
|
{
|
||||||
|
moveMouseToControlPoint(4);
|
||||||
|
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
|
||||||
|
addMovementStep(new Vector2(350, 0.01f));
|
||||||
|
assertControlPointType(2, PathType.Bezier);
|
||||||
|
|
||||||
|
addMovementStep(new Vector2(150, 150));
|
||||||
|
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
assertControlPointPosition(4, new Vector2(150, 150));
|
||||||
|
assertControlPointType(2, PathType.PerfectCurve);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDragControlPointPathAfterChangingType()
|
||||||
|
{
|
||||||
|
AddStep("change type to bezier", () => slider.Path.ControlPoints[2].Type.Value = PathType.Bezier);
|
||||||
|
AddStep("add point", () => slider.Path.ControlPoints.Add(new PathControlPoint(new Vector2(500, 10))));
|
||||||
|
AddStep("change type to perfect", () => slider.Path.ControlPoints[3].Type.Value = PathType.PerfectCurve);
|
||||||
|
|
||||||
|
moveMouseToControlPoint(4);
|
||||||
|
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
|
||||||
|
assertControlPointType(3, PathType.PerfectCurve);
|
||||||
|
|
||||||
|
addMovementStep(new Vector2(350, 0.01f));
|
||||||
|
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
|
||||||
|
assertControlPointPosition(4, new Vector2(350, 0.01f));
|
||||||
|
assertControlPointType(3, PathType.Bezier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addMovementStep(Vector2 relativePosition)
|
||||||
|
{
|
||||||
|
AddStep($"move mouse to {relativePosition}", () =>
|
||||||
|
{
|
||||||
|
Vector2 position = slider.Position + relativePosition;
|
||||||
|
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void moveMouseToControlPoint(int index)
|
||||||
|
{
|
||||||
|
AddStep($"move mouse to control point {index}", () =>
|
||||||
|
{
|
||||||
|
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
|
||||||
|
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertControlPointType(int index, PathType type) => AddAssert($"control point {index} is {type}", () => slider.Path.ControlPoints[index].Type.Value == type);
|
||||||
|
|
||||||
|
private void assertControlPointPosition(int index, Vector2 position) =>
|
||||||
|
AddAssert($"control point {index} at {position}", () => Precision.AlmostEquals(position, slider.Path.ControlPoints[index].Position.Value, 1));
|
||||||
|
|
||||||
|
private class TestSliderBlueprint : SliderSelectionBlueprint
|
||||||
|
{
|
||||||
|
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||||
|
public new TestSliderCircleBlueprint HeadBlueprint => (TestSliderCircleBlueprint)base.HeadBlueprint;
|
||||||
|
public new TestSliderCircleBlueprint TailBlueprint => (TestSliderCircleBlueprint)base.TailBlueprint;
|
||||||
|
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
|
||||||
|
|
||||||
|
public TestSliderBlueprint(DrawableSlider slider)
|
||||||
|
: base(slider)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new TestSliderCircleBlueprint(slider, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestSliderCircleBlueprint : SliderCircleSelectionBlueprint
|
||||||
|
{
|
||||||
|
public new HitCirclePiece CirclePiece => base.CirclePiece;
|
||||||
|
|
||||||
|
public TestSliderCircleBlueprint(DrawableSlider slider, SliderPosition position)
|
||||||
|
: base(slider, position)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -276,6 +276,104 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
assertControlPointType(0, PathType.Linear);
|
assertControlPointType(0, PathType.Linear);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacePerfectCurveSegmentAlmostLinearlyExterior()
|
||||||
|
{
|
||||||
|
Vector2 startPosition = new Vector2(200);
|
||||||
|
|
||||||
|
addMovementStep(startPosition);
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
addMovementStep(startPosition + new Vector2(300, 0));
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
addMovementStep(startPosition + new Vector2(150, 0.1f));
|
||||||
|
addClickStep(MouseButton.Right);
|
||||||
|
|
||||||
|
assertPlaced(true);
|
||||||
|
assertControlPointCount(3);
|
||||||
|
assertControlPointType(0, PathType.Bezier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacePerfectCurveSegmentRecovery()
|
||||||
|
{
|
||||||
|
Vector2 startPosition = new Vector2(200);
|
||||||
|
|
||||||
|
addMovementStep(startPosition);
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
addMovementStep(startPosition + new Vector2(300, 0));
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
addMovementStep(startPosition + new Vector2(150, 0.1f)); // Should convert to bezier
|
||||||
|
addMovementStep(startPosition + new Vector2(400.0f, 50.0f)); // Should convert back to perfect
|
||||||
|
addClickStep(MouseButton.Right);
|
||||||
|
|
||||||
|
assertPlaced(true);
|
||||||
|
assertControlPointCount(3);
|
||||||
|
assertControlPointType(0, PathType.PerfectCurve);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacePerfectCurveSegmentLarge()
|
||||||
|
{
|
||||||
|
Vector2 startPosition = new Vector2(400);
|
||||||
|
|
||||||
|
addMovementStep(startPosition);
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
addMovementStep(startPosition + new Vector2(220, 220));
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
// Playfield dimensions are 640 x 480.
|
||||||
|
// So a 440 x 440 bounding box should be ok.
|
||||||
|
addMovementStep(startPosition + new Vector2(-220, 220));
|
||||||
|
addClickStep(MouseButton.Right);
|
||||||
|
|
||||||
|
assertPlaced(true);
|
||||||
|
assertControlPointCount(3);
|
||||||
|
assertControlPointType(0, PathType.PerfectCurve);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacePerfectCurveSegmentTooLarge()
|
||||||
|
{
|
||||||
|
Vector2 startPosition = new Vector2(480, 200);
|
||||||
|
|
||||||
|
addMovementStep(startPosition);
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
addMovementStep(startPosition + new Vector2(400, 400));
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
// Playfield dimensions are 640 x 480.
|
||||||
|
// So an 800 * 800 bounding box area should not be ok.
|
||||||
|
addMovementStep(startPosition + new Vector2(-400, 400));
|
||||||
|
addClickStep(MouseButton.Right);
|
||||||
|
|
||||||
|
assertPlaced(true);
|
||||||
|
assertControlPointCount(3);
|
||||||
|
assertControlPointType(0, PathType.Bezier);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlacePerfectCurveSegmentCompleteArc()
|
||||||
|
{
|
||||||
|
addMovementStep(new Vector2(400));
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
addMovementStep(new Vector2(600, 400));
|
||||||
|
addClickStep(MouseButton.Left);
|
||||||
|
|
||||||
|
addMovementStep(new Vector2(400, 410));
|
||||||
|
addClickStep(MouseButton.Right);
|
||||||
|
|
||||||
|
assertPlaced(true);
|
||||||
|
assertControlPointCount(3);
|
||||||
|
assertControlPointType(0, PathType.PerfectCurve);
|
||||||
|
}
|
||||||
|
|
||||||
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
private void addMovementStep(Vector2 position) => AddStep($"move mouse to {position}", () => InputManager.MoveMouseTo(InputManager.ToScreenSpace(position)));
|
||||||
|
|
||||||
private void addClickStep(MouseButton button)
|
private void addClickStep(MouseButton button)
|
||||||
|
@ -2,14 +2,18 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
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.Cursor;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -28,6 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip
|
public class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip
|
||||||
{
|
{
|
||||||
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
|
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
|
||||||
|
public List<PathControlPoint> PointsInSegment;
|
||||||
|
|
||||||
public readonly BindableBool IsSelected = new BindableBool();
|
public readonly BindableBool IsSelected = new BindableBool();
|
||||||
public readonly PathControlPoint ControlPoint;
|
public readonly PathControlPoint ControlPoint;
|
||||||
@ -54,6 +59,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
this.slider = slider;
|
this.slider = slider;
|
||||||
ControlPoint = controlPoint;
|
ControlPoint = controlPoint;
|
||||||
|
|
||||||
|
slider.Path.Version.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
|
||||||
|
updatePathType();
|
||||||
|
}, runOnceImmediately: true);
|
||||||
|
|
||||||
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
|
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
|
||||||
|
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
@ -150,6 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
|
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
|
||||||
|
|
||||||
private Vector2 dragStartPosition;
|
private Vector2 dragStartPosition;
|
||||||
|
private PathType? dragPathType;
|
||||||
|
|
||||||
protected override bool OnDragStart(DragStartEvent e)
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
{
|
{
|
||||||
@ -159,6 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
if (e.Button == MouseButton.Left)
|
if (e.Button == MouseButton.Left)
|
||||||
{
|
{
|
||||||
dragStartPosition = ControlPoint.Position.Value;
|
dragStartPosition = ControlPoint.Position.Value;
|
||||||
|
dragPathType = PointsInSegment[0].Type.Value;
|
||||||
|
|
||||||
changeHandler?.BeginChange();
|
changeHandler?.BeginChange();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -184,10 +198,33 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
|
ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition);
|
||||||
|
|
||||||
|
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
|
||||||
|
PointsInSegment[0].Type.Value = dragPathType;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
|
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles correction of invalid path types.
|
||||||
|
/// </summary>
|
||||||
|
private void updatePathType()
|
||||||
|
{
|
||||||
|
if (ControlPoint.Type.Value != PathType.PerfectCurve)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (PointsInSegment.Count > 3)
|
||||||
|
ControlPoint.Type.Value = PathType.Bezier;
|
||||||
|
|
||||||
|
if (PointsInSegment.Count != 3)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position.Value).ToArray();
|
||||||
|
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||||
|
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||||
|
ControlPoint.Type.Value = PathType.Bezier;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the state of the circular control point marker.
|
/// Updates the state of the circular control point marker.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -153,6 +153,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to set the given control point piece to the given path type.
|
||||||
|
/// If that would fail, try to change the path such that it instead succeeds
|
||||||
|
/// in a UX-friendly way.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="piece">The control point piece that we want to change the path type of.</param>
|
||||||
|
/// <param name="type">The path type we want to assign to the given control point piece.</param>
|
||||||
|
private void updatePathType(PathControlPointPiece piece, PathType? type)
|
||||||
|
{
|
||||||
|
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case PathType.PerfectCurve:
|
||||||
|
// Can't always create a circular arc out of 4 or more points,
|
||||||
|
// so we split the segment into one 3-point circular arc segment
|
||||||
|
// and one segment of the previous type.
|
||||||
|
int thirdPointIndex = indexInSegment + 2;
|
||||||
|
|
||||||
|
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
|
||||||
|
piece.PointsInSegment[thirdPointIndex].Type.Value = piece.PointsInSegment[0].Type.Value;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
piece.ControlPoint.Type.Value = type;
|
||||||
|
}
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private IEditorChangeHandler changeHandler { get; set; }
|
private IEditorChangeHandler changeHandler { get; set; }
|
||||||
|
|
||||||
@ -218,7 +246,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
var item = new PathTypeMenuItem(type, () =>
|
var item = new PathTypeMenuItem(type, () =>
|
||||||
{
|
{
|
||||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||||
p.ControlPoint.Type.Value = type;
|
updatePathType(p, type);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (countOfState == totalCount)
|
if (countOfState == totalCount)
|
||||||
|
@ -142,6 +142,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
updateSlider();
|
updateSlider();
|
||||||
|
|
||||||
|
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
|
||||||
|
updatePathType();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePathType()
|
private void updatePathType()
|
||||||
|
@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
base.OnOperationEnded();
|
base.OnOperationEnded();
|
||||||
referenceOrigin = null;
|
referenceOrigin = null;
|
||||||
|
referencePathTypes = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
||||||
@ -53,6 +54,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private Vector2? referenceOrigin;
|
private Vector2? referenceOrigin;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// During a transform, the initial path types of a single selected slider are stored so they
|
||||||
|
/// can be maintained throughout the operation.
|
||||||
|
/// </summary>
|
||||||
|
private List<PathType?> referencePathTypes;
|
||||||
|
|
||||||
public override bool HandleReverse()
|
public override bool HandleReverse()
|
||||||
{
|
{
|
||||||
var hitObjects = EditorBeatmap.SelectedHitObjects;
|
var hitObjects = EditorBeatmap.SelectedHitObjects;
|
||||||
@ -194,6 +201,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
private void scaleSlider(Slider slider, Vector2 scale)
|
private void scaleSlider(Slider slider, Vector2 scale)
|
||||||
{
|
{
|
||||||
|
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList();
|
||||||
|
|
||||||
Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
|
Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
|
||||||
|
|
||||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
||||||
@ -209,6 +218,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
point.Position.Value *= pathRelativeDeltaScale;
|
point.Position.Value *= pathRelativeDeltaScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Maintain the path types in case they were defaulted to bezier at some point during scaling
|
||||||
|
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
|
||||||
|
slider.Path.ControlPoints[i].Type.Value = referencePathTypes[i];
|
||||||
|
|
||||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||||
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
|
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
|
||||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||||
|
@ -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 System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -108,16 +109,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
protected override void LoadSamples()
|
protected override void LoadSamples()
|
||||||
{
|
{
|
||||||
base.LoadSamples();
|
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
|
||||||
|
|
||||||
var firstSample = HitObject.Samples.FirstOrDefault();
|
if (HitObject.SampleControlPoint == null)
|
||||||
|
|
||||||
if (firstSample != null)
|
|
||||||
{
|
{
|
||||||
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide");
|
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
|
||||||
|
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
|
||||||
slidingSample.Samples = new ISampleInfo[] { clone };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
|
||||||
|
|
||||||
|
var slidingSamples = new List<ISampleInfo>();
|
||||||
|
|
||||||
|
var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
|
||||||
|
if (normalSample != null)
|
||||||
|
slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide"));
|
||||||
|
|
||||||
|
var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
|
||||||
|
if (whistleSample != null)
|
||||||
|
slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle"));
|
||||||
|
|
||||||
|
slidingSample.Samples = slidingSamples.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void StopAllSamples()
|
public override void StopAllSamples()
|
||||||
|
@ -81,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
public List<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public IList<HitSampleInfo> TailSamples { get; private set; }
|
||||||
|
|
||||||
private int repeatCount;
|
private int repeatCount;
|
||||||
|
|
||||||
public int RepeatCount
|
public int RepeatCount
|
||||||
@ -143,11 +146,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||||
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
|
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
|
||||||
|
|
||||||
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
|
|
||||||
// For now, the samples are attached to and played by the slider itself at the correct end time.
|
|
||||||
// ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live).
|
|
||||||
Samples = this.GetNodeSamples(repeatCount + 1).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
@ -238,6 +236,10 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
if (HeadCircle != null)
|
if (HeadCircle != null)
|
||||||
HeadCircle.Samples = this.GetNodeSamples(0);
|
HeadCircle.Samples = this.GetNodeSamples(0);
|
||||||
|
|
||||||
|
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
|
||||||
|
// For now, the samples are played by the slider itself at the correct end time.
|
||||||
|
TailSamples = this.GetNodeSamples(repeatCount + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
|
public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
|
||||||
|
@ -1,115 +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 NUnit.Framework;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.NonVisual
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class LimitedCapacityStackTest
|
|
||||||
{
|
|
||||||
private const int capacity = 3;
|
|
||||||
|
|
||||||
private LimitedCapacityStack<int> stack;
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
stack = new LimitedCapacityStack<int>(capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestEmptyStack()
|
|
||||||
{
|
|
||||||
Assert.AreEqual(0, stack.Count);
|
|
||||||
|
|
||||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
||||||
{
|
|
||||||
int unused = stack[0];
|
|
||||||
});
|
|
||||||
|
|
||||||
int count = 0;
|
|
||||||
foreach (var unused in stack)
|
|
||||||
count++;
|
|
||||||
|
|
||||||
Assert.AreEqual(0, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1)]
|
|
||||||
[TestCase(2)]
|
|
||||||
[TestCase(3)]
|
|
||||||
public void TestInRangeElements(int count)
|
|
||||||
{
|
|
||||||
// e.g. 0 -> 1 -> 2
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
stack.Push(i);
|
|
||||||
|
|
||||||
Assert.AreEqual(count, stack.Count);
|
|
||||||
|
|
||||||
// e.g. 2 -> 1 -> 0 (reverse order)
|
|
||||||
for (int i = 0; i < stack.Count; i++)
|
|
||||||
Assert.AreEqual(count - 1 - i, stack[i]);
|
|
||||||
|
|
||||||
// e.g. indices 3, 4, 5, 6 (out of range)
|
|
||||||
for (int i = stack.Count; i < stack.Count + capacity; i++)
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
||||||
{
|
|
||||||
int unused = stack[i];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(4)]
|
|
||||||
[TestCase(5)]
|
|
||||||
[TestCase(6)]
|
|
||||||
public void TestOverflowElements(int count)
|
|
||||||
{
|
|
||||||
// e.g. 0 -> 1 -> 2 -> 3
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
stack.Push(i);
|
|
||||||
|
|
||||||
Assert.AreEqual(capacity, stack.Count);
|
|
||||||
|
|
||||||
// e.g. 3 -> 2 -> 1 (reverse order)
|
|
||||||
for (int i = 0; i < stack.Count; i++)
|
|
||||||
Assert.AreEqual(count - 1 - i, stack[i]);
|
|
||||||
|
|
||||||
// e.g. indices 3, 4, 5, 6 (out of range)
|
|
||||||
for (int i = stack.Count; i < stack.Count + capacity; i++)
|
|
||||||
{
|
|
||||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
|
||||||
{
|
|
||||||
int unused = stack[i];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(1)]
|
|
||||||
[TestCase(2)]
|
|
||||||
[TestCase(3)]
|
|
||||||
[TestCase(4)]
|
|
||||||
[TestCase(5)]
|
|
||||||
[TestCase(6)]
|
|
||||||
public void TestEnumerator(int count)
|
|
||||||
{
|
|
||||||
// e.g. 0 -> 1 -> 2 -> 3
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
stack.Push(i);
|
|
||||||
|
|
||||||
int enumeratorCount = 0;
|
|
||||||
int expectedValue = count - 1;
|
|
||||||
|
|
||||||
foreach (var item in stack)
|
|
||||||
{
|
|
||||||
Assert.AreEqual(expectedValue, item);
|
|
||||||
enumeratorCount++;
|
|
||||||
expectedValue--;
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.AreEqual(stack.Count, enumeratorCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
143
osu.Game.Tests/NonVisual/ReverseQueueTest.cs
Normal file
143
osu.Game.Tests/NonVisual/ReverseQueueTest.cs
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.NonVisual
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class ReverseQueueTest
|
||||||
|
{
|
||||||
|
private ReverseQueue<char> queue;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
queue = new ReverseQueue<char>(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEmptyQueue()
|
||||||
|
{
|
||||||
|
Assert.AreEqual(0, queue.Count);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||||
|
{
|
||||||
|
char unused = queue[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
foreach (var unused in queue)
|
||||||
|
count++;
|
||||||
|
|
||||||
|
Assert.AreEqual(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEnqueue()
|
||||||
|
{
|
||||||
|
// Assert correct values and reverse index after enqueueing
|
||||||
|
queue.Enqueue('a');
|
||||||
|
queue.Enqueue('b');
|
||||||
|
queue.Enqueue('c');
|
||||||
|
|
||||||
|
Assert.AreEqual('c', queue[0]);
|
||||||
|
Assert.AreEqual('b', queue[1]);
|
||||||
|
Assert.AreEqual('a', queue[2]);
|
||||||
|
|
||||||
|
// Assert correct values and reverse index after enqueueing beyond initial capacity of 4
|
||||||
|
queue.Enqueue('d');
|
||||||
|
queue.Enqueue('e');
|
||||||
|
queue.Enqueue('f');
|
||||||
|
|
||||||
|
Assert.AreEqual('f', queue[0]);
|
||||||
|
Assert.AreEqual('e', queue[1]);
|
||||||
|
Assert.AreEqual('d', queue[2]);
|
||||||
|
Assert.AreEqual('c', queue[3]);
|
||||||
|
Assert.AreEqual('b', queue[4]);
|
||||||
|
Assert.AreEqual('a', queue[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDequeue()
|
||||||
|
{
|
||||||
|
queue.Enqueue('a');
|
||||||
|
queue.Enqueue('b');
|
||||||
|
queue.Enqueue('c');
|
||||||
|
queue.Enqueue('d');
|
||||||
|
queue.Enqueue('e');
|
||||||
|
queue.Enqueue('f');
|
||||||
|
|
||||||
|
// Assert correct item return and no longer in queue after dequeueing
|
||||||
|
Assert.AreEqual('a', queue[5]);
|
||||||
|
var dequeuedItem = queue.Dequeue();
|
||||||
|
|
||||||
|
Assert.AreEqual('a', dequeuedItem);
|
||||||
|
Assert.AreEqual(5, queue.Count);
|
||||||
|
Assert.AreEqual('f', queue[0]);
|
||||||
|
Assert.AreEqual('b', queue[4]);
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||||
|
{
|
||||||
|
char unused = queue[5];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert correct state after enough enqueues and dequeues to wrap around array (queue.start = 0 again)
|
||||||
|
queue.Enqueue('g');
|
||||||
|
queue.Enqueue('h');
|
||||||
|
queue.Enqueue('i');
|
||||||
|
queue.Dequeue();
|
||||||
|
queue.Dequeue();
|
||||||
|
queue.Dequeue();
|
||||||
|
queue.Dequeue();
|
||||||
|
queue.Dequeue();
|
||||||
|
queue.Dequeue();
|
||||||
|
queue.Dequeue();
|
||||||
|
|
||||||
|
Assert.AreEqual(1, queue.Count);
|
||||||
|
Assert.AreEqual('i', queue[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClear()
|
||||||
|
{
|
||||||
|
queue.Enqueue('a');
|
||||||
|
queue.Enqueue('b');
|
||||||
|
queue.Enqueue('c');
|
||||||
|
queue.Enqueue('d');
|
||||||
|
queue.Enqueue('e');
|
||||||
|
queue.Enqueue('f');
|
||||||
|
|
||||||
|
// Assert queue is empty after clearing
|
||||||
|
queue.Clear();
|
||||||
|
|
||||||
|
Assert.AreEqual(0, queue.Count);
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||||
|
{
|
||||||
|
char unused = queue[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEnumerator()
|
||||||
|
{
|
||||||
|
queue.Enqueue('a');
|
||||||
|
queue.Enqueue('b');
|
||||||
|
queue.Enqueue('c');
|
||||||
|
queue.Enqueue('d');
|
||||||
|
queue.Enqueue('e');
|
||||||
|
queue.Enqueue('f');
|
||||||
|
|
||||||
|
char[] expectedValues = { 'f', 'e', 'd', 'c', 'b', 'a' };
|
||||||
|
int expectedValueIndex = 0;
|
||||||
|
|
||||||
|
// Assert items are enumerated in correct order
|
||||||
|
foreach (var item in queue)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(expectedValues[expectedValueIndex], item);
|
||||||
|
expectedValueIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuGameBase game)
|
private void load(OsuGameBase game)
|
||||||
{
|
{
|
||||||
Child = globalActionContainer = new GlobalActionContainer(game, null);
|
Child = globalActionContainer = new GlobalActionContainer(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
|
{
|
||||||
|
public class TestSceneMultiplayerMatchFooter : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
private readonly OnlinePlayBeatmapAvailabilityTracker availablilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = new MultiplayerMatchFooter
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Height = 50
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,21 @@
|
|||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osu.Game.Users;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
@ -18,11 +26,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
private MultiplayerMatchSubScreen screen;
|
private MultiplayerMatchSubScreen screen;
|
||||||
|
|
||||||
|
private BeatmapManager beatmaps;
|
||||||
|
private RulesetStore rulesets;
|
||||||
|
private BeatmapSetInfo importedSet;
|
||||||
|
|
||||||
public TestSceneMultiplayerMatchSubScreen()
|
public TestSceneMultiplayerMatchSubScreen()
|
||||||
: base(false)
|
: base(false)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(GameHost host, AudioManager audio)
|
||||||
|
{
|
||||||
|
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
|
||||||
|
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
|
||||||
|
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
|
||||||
|
|
||||||
|
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
|
||||||
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public new void Setup() => Schedule(() =>
|
public new void Setup() => Schedule(() =>
|
||||||
{
|
{
|
||||||
@ -71,7 +93,48 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddWaitStep("wait", 10);
|
AddUntilStep("wait for join", () => Client.Room != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStartMatchWhileSpectating()
|
||||||
|
{
|
||||||
|
AddStep("set playlist", () =>
|
||||||
|
{
|
||||||
|
Room.Playlist.Add(new PlaylistItem
|
||||||
|
{
|
||||||
|
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo },
|
||||||
|
Ruleset = { Value = new OsuRuleset().RulesetInfo },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("click create button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for room join", () => Client.Room != null);
|
||||||
|
|
||||||
|
AddStep("join other user (ready)", () =>
|
||||||
|
{
|
||||||
|
Client.AddUser(new User { Id = 55 });
|
||||||
|
Client.ChangeUserState(55, MultiplayerUserState.Ready);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("click spectate button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerSpectateButton>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("click ready button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerReadyButton>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,6 +126,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddUntilStep("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().Single().IsPresent);
|
AddUntilStep("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().Single().IsPresent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestToggleSpectateState()
|
||||||
|
{
|
||||||
|
AddStep("make user spectating", () => Client.ChangeState(MultiplayerUserState.Spectating));
|
||||||
|
AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCrownChangesStateWhenHostTransferred()
|
public void TestCrownChangesStateWhenHostTransferred()
|
||||||
{
|
{
|
||||||
|
@ -209,9 +209,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
addClickButtonStep();
|
addClickButtonStep();
|
||||||
AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
|
AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
|
||||||
AddAssert("ready button disabled", () => !button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
|
||||||
|
|
||||||
|
AddAssert("ready button disabled", () => !button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||||
AddStep("transitioned to gameplay", () => readyClickOperation.Dispose());
|
AddStep("transitioned to gameplay", () => readyClickOperation.Dispose());
|
||||||
|
|
||||||
|
AddStep("finish gameplay", () =>
|
||||||
|
{
|
||||||
|
Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
|
||||||
|
Client.ChangeUserState(Client.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
|
||||||
|
});
|
||||||
|
|
||||||
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,193 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
|
{
|
||||||
|
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
private MultiplayerSpectateButton spectateButton;
|
||||||
|
private MultiplayerReadyButton readyButton;
|
||||||
|
|
||||||
|
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
|
private BeatmapSetInfo importedSet;
|
||||||
|
private BeatmapManager beatmaps;
|
||||||
|
private RulesetStore rulesets;
|
||||||
|
|
||||||
|
private IDisposable readyClickOperation;
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
|
public TestSceneMultiplayerSpectateButton()
|
||||||
|
{
|
||||||
|
base.Content.Add(content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
|
{
|
||||||
|
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(GameHost host, AudioManager audio)
|
||||||
|
{
|
||||||
|
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
|
||||||
|
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
|
||||||
|
|
||||||
|
var beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = selectedItem } };
|
||||||
|
base.Content.Add(beatmapTracker);
|
||||||
|
Dependencies.Cache(beatmapTracker);
|
||||||
|
|
||||||
|
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public new void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
|
||||||
|
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
||||||
|
selectedItem.Value = new PlaylistItem
|
||||||
|
{
|
||||||
|
Beatmap = { Value = Beatmap.Value.BeatmapInfo },
|
||||||
|
Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset },
|
||||||
|
};
|
||||||
|
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
spectateButton = new MultiplayerSpectateButton
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(200, 50),
|
||||||
|
OnSpectateClick = async () =>
|
||||||
|
{
|
||||||
|
readyClickOperation = OngoingOperationTracker.BeginOperation();
|
||||||
|
await Client.ToggleSpectate();
|
||||||
|
readyClickOperation.Dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readyButton = new MultiplayerReadyButton
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(200, 50),
|
||||||
|
OnReadyClick = async () =>
|
||||||
|
{
|
||||||
|
readyClickOperation = OngoingOperationTracker.BeginOperation();
|
||||||
|
|
||||||
|
if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
|
||||||
|
{
|
||||||
|
await Client.StartMatch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Client.ToggleReady();
|
||||||
|
readyClickOperation.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEnabledWhenRoomOpen()
|
||||||
|
{
|
||||||
|
assertSpectateButtonEnablement(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(MultiplayerUserState.Idle)]
|
||||||
|
[TestCase(MultiplayerUserState.Ready)]
|
||||||
|
public void TestToggleWhenIdle(MultiplayerUserState initialState)
|
||||||
|
{
|
||||||
|
addClickSpectateButtonStep();
|
||||||
|
AddAssert("user is spectating", () => Client.Room?.Users[0].State == MultiplayerUserState.Spectating);
|
||||||
|
|
||||||
|
addClickSpectateButtonStep();
|
||||||
|
AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(MultiplayerRoomState.WaitingForLoad)]
|
||||||
|
[TestCase(MultiplayerRoomState.Playing)]
|
||||||
|
[TestCase(MultiplayerRoomState.Closed)]
|
||||||
|
public void TestDisabledDuringGameplayOrClosed(MultiplayerRoomState roomState)
|
||||||
|
{
|
||||||
|
AddStep($"change user to {roomState}", () => Client.ChangeRoomState(roomState));
|
||||||
|
assertSpectateButtonEnablement(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReadyButtonDisabledWhenHostAndNoReadyUsers()
|
||||||
|
{
|
||||||
|
addClickSpectateButtonStep();
|
||||||
|
assertReadyButtonEnablement(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReadyButtonEnabledWhenHostAndUsersReady()
|
||||||
|
{
|
||||||
|
AddStep("add user", () => Client.AddUser(new User { Id = 55 }));
|
||||||
|
AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready));
|
||||||
|
|
||||||
|
addClickSpectateButtonStep();
|
||||||
|
assertReadyButtonEnablement(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReadyButtonDisabledWhenNotHostAndUsersReady()
|
||||||
|
{
|
||||||
|
AddStep("add user and transfer host", () =>
|
||||||
|
{
|
||||||
|
Client.AddUser(new User { Id = 55 });
|
||||||
|
Client.TransferHost(55);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready));
|
||||||
|
|
||||||
|
addClickSpectateButtonStep();
|
||||||
|
assertReadyButtonEnablement(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addClickSpectateButtonStep() => AddStep("click spectate button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(spectateButton);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
private void assertSpectateButtonEnablement(bool shouldBeEnabled)
|
||||||
|
=> AddAssert($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
|
||||||
|
|
||||||
|
private void assertReadyButtonEnablement(bool shouldBeEnabled)
|
||||||
|
=> AddAssert($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
|
||||||
|
}
|
||||||
|
}
|
@ -44,6 +44,20 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
exitViaEscapeAndConfirm();
|
exitViaEscapeAndConfirm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding
|
||||||
|
/// but should be handled *after* song select).
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestOpenModSelectOverlayUsingAction()
|
||||||
|
{
|
||||||
|
TestSongSelect songSelect = null;
|
||||||
|
|
||||||
|
PushAndConfirm(() => songSelect = new TestSongSelect());
|
||||||
|
AddStep("Show mods overlay", () => InputManager.Key(Key.F1));
|
||||||
|
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRetryCountIncrements()
|
public void TestRetryCountIncrements()
|
||||||
{
|
{
|
||||||
|
@ -27,8 +27,6 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public IBeatmap Beatmap { get; }
|
public IBeatmap Beatmap { get; }
|
||||||
|
|
||||||
private CancellationToken cancellationToken;
|
|
||||||
|
|
||||||
protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
|
protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset)
|
||||||
{
|
{
|
||||||
Beatmap = beatmap;
|
Beatmap = beatmap;
|
||||||
@ -41,8 +39,6 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public IBeatmap Convert(CancellationToken cancellationToken = default)
|
public IBeatmap Convert(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
this.cancellationToken = cancellationToken;
|
|
||||||
|
|
||||||
// We always operate on a clone of the original beatmap, to not modify it game-wide
|
// We always operate on a clone of the original beatmap, to not modify it game-wide
|
||||||
return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
|
return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
|
||||||
}
|
}
|
||||||
@ -55,19 +51,6 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <returns>The converted Beatmap.</returns>
|
/// <returns>The converted Beatmap.</returns>
|
||||||
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
#pragma warning disable 618
|
|
||||||
return ConvertBeatmap(original);
|
|
||||||
#pragma warning restore 618
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs the conversion of a Beatmap using this Beatmap Converter.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="original">The un-converted Beatmap.</param>
|
|
||||||
/// <returns>The converted Beatmap.</returns>
|
|
||||||
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
|
|
||||||
protected virtual Beatmap<T> ConvertBeatmap(IBeatmap original)
|
|
||||||
{
|
|
||||||
var beatmap = CreateBeatmap();
|
var beatmap = CreateBeatmap();
|
||||||
|
|
||||||
beatmap.BeatmapInfo = original.BeatmapInfo;
|
beatmap.BeatmapInfo = original.BeatmapInfo;
|
||||||
@ -121,21 +104,6 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <param name="beatmap">The un-converted Beatmap.</param>
|
/// <param name="beatmap">The un-converted Beatmap.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>The converted hit object.</returns>
|
/// <returns>The converted hit object.</returns>
|
||||||
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)
|
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) => Enumerable.Empty<T>();
|
||||||
{
|
|
||||||
#pragma warning disable 618
|
|
||||||
return ConvertHitObject(original, beatmap);
|
|
||||||
#pragma warning restore 618
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs the conversion of a hit object.
|
|
||||||
/// This method is generally executed sequentially for all objects in a beatmap.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="original">The hit object to convert.</param>
|
|
||||||
/// <param name="beatmap">The un-converted Beatmap.</param>
|
|
||||||
/// <returns>The converted hit object.</returns>
|
|
||||||
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
|
|
||||||
protected virtual IEnumerable<T> ConvertHitObject(HitObject original, IBeatmap beatmap) => Enumerable.Empty<T>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
public class BeatmapStatistic
|
public class BeatmapStatistic
|
||||||
{
|
{
|
||||||
[Obsolete("Use CreateIcon instead")] // can be removed 20210203
|
|
||||||
public IconUsage Icon = FontAwesome.Regular.QuestionCircle;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A function to create the icon for display purposes. Use default icons available via <see cref="BeatmapStatisticIcon"/> whenever possible for conformity.
|
/// A function to create the icon for display purposes. Use default icons available via <see cref="BeatmapStatisticIcon"/> whenever possible for conformity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -20,12 +15,5 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public string Content;
|
public string Content;
|
||||||
public string Name;
|
public string Name;
|
||||||
|
|
||||||
public BeatmapStatistic()
|
|
||||||
{
|
|
||||||
#pragma warning disable 618
|
|
||||||
CreateIcon = () => new SpriteIcon { Icon = Icon, Scale = new Vector2(0.7f) };
|
|
||||||
#pragma warning restore 618
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -273,7 +273,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
if (hitObject is IHasPath path)
|
if (hitObject is IHasPath path)
|
||||||
{
|
{
|
||||||
addPathData(writer, path, position);
|
addPathData(writer, path, position);
|
||||||
writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true));
|
writer.Write(getSampleBank(hitObject.Samples));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -420,15 +420,15 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}"));
|
writer.Write(FormattableString.Invariant($"{endTimeData.EndTime}{suffix}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false, bool zeroBanks = false)
|
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false)
|
||||||
{
|
{
|
||||||
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
|
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
|
||||||
LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank);
|
LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank);
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)normalBank)}:"));
|
sb.Append(FormattableString.Invariant($"{(int)normalBank}:"));
|
||||||
sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)addBank)}"));
|
sb.Append(FormattableString.Invariant($"{(int)addBank}"));
|
||||||
|
|
||||||
if (!banksOnly)
|
if (!banksOnly)
|
||||||
{
|
{
|
||||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
public override void PlayHoverSample()
|
public override void PlayHoverSample()
|
||||||
{
|
{
|
||||||
sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08);
|
sampleHover.Frequency.Value = 0.98 + RNG.NextDouble(0.04);
|
||||||
sampleHover.Play();
|
sampleHover.Play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,6 +53,9 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Soft,
|
Soft,
|
||||||
|
|
||||||
[Description("-toolbar")]
|
[Description("-toolbar")]
|
||||||
Toolbar
|
Toolbar,
|
||||||
|
|
||||||
|
[Description("-songselect")]
|
||||||
|
SongSelect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
@ -13,20 +12,23 @@ namespace osu.Game.Input.Bindings
|
|||||||
{
|
{
|
||||||
public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput
|
public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput
|
||||||
{
|
{
|
||||||
[CanBeNull]
|
|
||||||
private readonly GlobalInputManager globalInputManager;
|
|
||||||
|
|
||||||
private readonly Drawable handler;
|
private readonly Drawable handler;
|
||||||
|
private InputManager parentInputManager;
|
||||||
|
|
||||||
public GlobalActionContainer(OsuGameBase game, [CanBeNull] GlobalInputManager globalInputManager)
|
public GlobalActionContainer(OsuGameBase game)
|
||||||
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
|
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
|
||||||
{
|
{
|
||||||
this.globalInputManager = globalInputManager;
|
|
||||||
|
|
||||||
if (game is IKeyBindingHandler<GlobalAction>)
|
if (game is IKeyBindingHandler<GlobalAction>)
|
||||||
handler = game;
|
handler = game;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
parentInputManager = GetContainingInputManager();
|
||||||
|
}
|
||||||
|
|
||||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
|
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
|
||||||
.Concat(EditorKeyBindings)
|
.Concat(EditorKeyBindings)
|
||||||
.Concat(InGameKeyBindings)
|
.Concat(InGameKeyBindings)
|
||||||
@ -113,7 +115,12 @@ namespace osu.Game.Input.Bindings
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var inputQueue = globalInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue;
|
// To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content.
|
||||||
|
// It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly
|
||||||
|
// allow the whole game to handle these actions.
|
||||||
|
|
||||||
|
// An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging.
|
||||||
|
var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue;
|
||||||
|
|
||||||
return handler != null ? inputQueue.Prepend(handler) : inputQueue;
|
return handler != null ? inputQueue.Prepend(handler) : inputQueue;
|
||||||
}
|
}
|
||||||
|
@ -1,29 +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 osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Input;
|
|
||||||
|
|
||||||
namespace osu.Game.Input.Bindings
|
|
||||||
{
|
|
||||||
public class GlobalInputManager : PassThroughInputManager
|
|
||||||
{
|
|
||||||
public readonly GlobalActionContainer GlobalBindings;
|
|
||||||
|
|
||||||
protected override Container<Drawable> Content { get; }
|
|
||||||
|
|
||||||
public GlobalInputManager(OsuGameBase game)
|
|
||||||
{
|
|
||||||
InternalChildren = new Drawable[]
|
|
||||||
{
|
|
||||||
Content = new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
// to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything.
|
|
||||||
GlobalBindings = new GlobalActionContainer(game, this)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -96,6 +96,9 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
if (!IsConnected.Value)
|
if (!IsConnected.Value)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
if (newState == MultiplayerUserState.Spectating)
|
||||||
|
return Task.CompletedTask; // Not supported yet.
|
||||||
|
|
||||||
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
|
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,5 +55,10 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
/// The user is currently viewing results. This is a reserved state, and is set by the server.
|
/// The user is currently viewing results. This is a reserved state, and is set by the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Results,
|
Results,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user is currently spectating this room.
|
||||||
|
/// </summary>
|
||||||
|
Spectating
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,6 +249,33 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggles the <see cref="LocalUser"/>'s spectating state.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="InvalidOperationException">If a toggle of the spectating state is not valid at this time.</exception>
|
||||||
|
public async Task ToggleSpectate()
|
||||||
|
{
|
||||||
|
var localUser = LocalUser;
|
||||||
|
|
||||||
|
if (localUser == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (localUser.State)
|
||||||
|
{
|
||||||
|
case MultiplayerUserState.Idle:
|
||||||
|
case MultiplayerUserState.Ready:
|
||||||
|
await ChangeState(MultiplayerUserState.Spectating).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Spectating:
|
||||||
|
await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Cannot toggle spectate when in {localUser.State}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public abstract Task TransferHost(int userId);
|
public abstract Task TransferHost(int userId);
|
||||||
|
|
||||||
public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
|
public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Globalization;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using osu.Framework.IO.Network;
|
using osu.Framework.IO.Network;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
@ -11,11 +12,13 @@ namespace osu.Game.Online.Solo
|
|||||||
public class CreateSoloScoreRequest : APIRequest<APIScoreToken>
|
public class CreateSoloScoreRequest : APIRequest<APIScoreToken>
|
||||||
{
|
{
|
||||||
private readonly int beatmapId;
|
private readonly int beatmapId;
|
||||||
|
private readonly int rulesetId;
|
||||||
private readonly string versionHash;
|
private readonly string versionHash;
|
||||||
|
|
||||||
public CreateSoloScoreRequest(int beatmapId, string versionHash)
|
public CreateSoloScoreRequest(int beatmapId, int rulesetId, string versionHash)
|
||||||
{
|
{
|
||||||
this.beatmapId = beatmapId;
|
this.beatmapId = beatmapId;
|
||||||
|
this.rulesetId = rulesetId;
|
||||||
this.versionHash = versionHash;
|
this.versionHash = versionHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,9 +27,10 @@ namespace osu.Game.Online.Solo
|
|||||||
var req = base.CreateWebRequest();
|
var req = base.CreateWebRequest();
|
||||||
req.Method = HttpMethod.Post;
|
req.Method = HttpMethod.Post;
|
||||||
req.AddParameter("version_hash", versionHash);
|
req.AddParameter("version_hash", versionHash);
|
||||||
|
req.AddParameter("ruleset_id", rulesetId.ToString(CultureInfo.InvariantCulture));
|
||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Target => $@"solo/{beatmapId}/scores";
|
protected override string Target => $@"beatmaps/{beatmapId}/solo/scores";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,6 @@ namespace osu.Game.Online.Solo
|
|||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override string Target => $@"solo/{beatmapId}/scores/{scoreId}";
|
protected override string Target => $@"beatmaps/{beatmapId}/solo/scores/{scoreId}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -311,18 +311,21 @@ namespace osu.Game
|
|||||||
|
|
||||||
AddInternal(RulesetConfigCache);
|
AddInternal(RulesetConfigCache);
|
||||||
|
|
||||||
var globalInput = new GlobalInputManager(this)
|
GlobalActionContainer globalBindings;
|
||||||
|
|
||||||
|
var mainContent = new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both },
|
||||||
Child = MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }
|
// to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything.
|
||||||
|
globalBindings = new GlobalActionContainer(this)
|
||||||
};
|
};
|
||||||
|
|
||||||
MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both };
|
MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
base.Content.Add(CreateScalingContainer().WithChild(globalInput));
|
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
|
||||||
|
|
||||||
KeyBindingStore.Register(globalInput.GlobalBindings);
|
KeyBindingStore.Register(globalBindings);
|
||||||
dependencies.Cache(globalInput.GlobalBindings);
|
dependencies.Cache(globalBindings);
|
||||||
|
|
||||||
PreviewTrackManager previewTrackManager;
|
PreviewTrackManager previewTrackManager;
|
||||||
dependencies.Cache(previewTrackManager = new PreviewTrackManager());
|
dependencies.Cache(previewTrackManager = new PreviewTrackManager());
|
||||||
|
@ -57,13 +57,6 @@ namespace osu.Game.Overlays.Settings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("Use Current instead")] // Can be removed 20210406
|
|
||||||
public Bindable<T> Bindable
|
|
||||||
{
|
|
||||||
get => Current;
|
|
||||||
set => Current = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual Bindable<T> Current
|
public virtual Bindable<T> Current
|
||||||
{
|
{
|
||||||
get => controlWithCurrent.Current;
|
get => controlWithCurrent.Current;
|
||||||
|
@ -16,7 +16,12 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// <see cref="DifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
|
/// <see cref="DifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet
|
protected readonly ReverseQueue<DifficultyHitObject> Previous;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of previous <see cref="DifficultyHitObject"/>s to keep inside the <see cref="Previous"/> queue.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual int HistoryLength => 1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mods for use in skill calculations.
|
/// Mods for use in skill calculations.
|
||||||
@ -28,12 +33,17 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
protected Skill(Mod[] mods)
|
protected Skill(Mod[] mods)
|
||||||
{
|
{
|
||||||
this.mods = mods;
|
this.mods = mods;
|
||||||
|
Previous = new ReverseQueue<DifficultyHitObject>(HistoryLength + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void ProcessInternal(DifficultyHitObject current)
|
internal void ProcessInternal(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
|
while (Previous.Count > HistoryLength)
|
||||||
|
Previous.Dequeue();
|
||||||
|
|
||||||
Process(current);
|
Process(current);
|
||||||
Previous.Push(current);
|
|
||||||
|
Previous.Enqueue(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,92 +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.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Difficulty.Utils
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// An indexed stack with limited depth. Indexing starts at the top of the stack.
|
|
||||||
/// </summary>
|
|
||||||
public class LimitedCapacityStack<T> : IEnumerable<T>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The number of elements in the stack.
|
|
||||||
/// </summary>
|
|
||||||
public int Count { get; private set; }
|
|
||||||
|
|
||||||
private readonly T[] array;
|
|
||||||
private readonly int capacity;
|
|
||||||
private int marker; // Marks the position of the most recently added item.
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constructs a new <see cref="LimitedCapacityStack{T}"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="capacity">The number of items the stack can hold.</param>
|
|
||||||
public LimitedCapacityStack(int capacity)
|
|
||||||
{
|
|
||||||
if (capacity < 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(capacity));
|
|
||||||
|
|
||||||
this.capacity = capacity;
|
|
||||||
array = new T[capacity];
|
|
||||||
marker = capacity; // Set marker to the end of the array, outside of the indexed range by one.
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the item at an index in the stack.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="i">The index of the item to retrieve. The top of the stack is returned at index 0.</param>
|
|
||||||
public T this[int i]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (i < 0 || i > Count - 1)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(i));
|
|
||||||
|
|
||||||
i += marker;
|
|
||||||
if (i > capacity - 1)
|
|
||||||
i -= capacity;
|
|
||||||
|
|
||||||
return array[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pushes an item to this <see cref="LimitedCapacityStack{T}"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The item to push.</param>
|
|
||||||
public void Push(T item)
|
|
||||||
{
|
|
||||||
// Overwrite the oldest item instead of shifting every item by one with every addition.
|
|
||||||
if (marker == 0)
|
|
||||||
marker = capacity - 1;
|
|
||||||
else
|
|
||||||
--marker;
|
|
||||||
|
|
||||||
array[marker] = item;
|
|
||||||
|
|
||||||
if (Count < capacity)
|
|
||||||
++Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns an enumerator which enumerates items in the history starting from the most recently added one.
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerator<T> GetEnumerator()
|
|
||||||
{
|
|
||||||
for (int i = marker; i < capacity; ++i)
|
|
||||||
yield return array[i];
|
|
||||||
|
|
||||||
if (Count == capacity)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < marker; ++i)
|
|
||||||
yield return array[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
|
||||||
}
|
|
||||||
}
|
|
133
osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs
Normal file
133
osu.Game/Rulesets/Difficulty/Utils/ReverseQueue.cs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Difficulty.Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An indexed queue where items are indexed beginning from the most recently enqueued item.
|
||||||
|
/// Enqueuing an item pushes all existing indexes up by one and inserts the item at index 0.
|
||||||
|
/// Dequeuing an item removes the item from the highest index and returns it.
|
||||||
|
/// </summary>
|
||||||
|
public class ReverseQueue<T> : IEnumerable<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The number of elements in the <see cref="ReverseQueue{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; private set; }
|
||||||
|
|
||||||
|
private T[] items;
|
||||||
|
private int capacity;
|
||||||
|
private int start;
|
||||||
|
|
||||||
|
public ReverseQueue(int initialCapacity)
|
||||||
|
{
|
||||||
|
if (initialCapacity <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(initialCapacity));
|
||||||
|
|
||||||
|
items = new T[initialCapacity];
|
||||||
|
capacity = initialCapacity;
|
||||||
|
start = 0;
|
||||||
|
Count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the item at an index in the <see cref="ReverseQueue{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The index of the item to retrieve. The most recently enqueued item is at index 0.</param>
|
||||||
|
public T this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (index < 0 || index > Count - 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
|
int reverseIndex = Count - 1 - index;
|
||||||
|
return items[(start + reverseIndex) % capacity];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enqueues an item to this <see cref="ReverseQueue{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item to enqueue.</param>
|
||||||
|
public void Enqueue(T item)
|
||||||
|
{
|
||||||
|
if (Count == capacity)
|
||||||
|
{
|
||||||
|
// Double the buffer size
|
||||||
|
var buffer = new T[capacity * 2];
|
||||||
|
|
||||||
|
// Copy items to new queue
|
||||||
|
for (int i = 0; i < Count; i++)
|
||||||
|
{
|
||||||
|
buffer[i] = items[(start + i) % capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace array with new buffer
|
||||||
|
items = buffer;
|
||||||
|
capacity *= 2;
|
||||||
|
start = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
items[(start + Count) % capacity] = item;
|
||||||
|
Count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dequeues the least recently enqueued item from the <see cref="ReverseQueue{T}"/> and returns it.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The item dequeued from the <see cref="ReverseQueue{T}"/>.</returns>
|
||||||
|
public T Dequeue()
|
||||||
|
{
|
||||||
|
var item = items[start];
|
||||||
|
start = (start + 1) % capacity;
|
||||||
|
Count--;
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the <see cref="ReverseQueue{T}"/> of all items.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
start = 0;
|
||||||
|
Count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an enumerator which enumerates items in the <see cref="ReverseQueue{T}"/> starting from the most recently enqueued item.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerator<T> GetEnumerator() => new Enumerator(this);
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
|
public struct Enumerator : IEnumerator<T>
|
||||||
|
{
|
||||||
|
private ReverseQueue<T> reverseQueue;
|
||||||
|
private int currentIndex;
|
||||||
|
|
||||||
|
internal Enumerator(ReverseQueue<T> reverseQueue)
|
||||||
|
{
|
||||||
|
this.reverseQueue = reverseQueue;
|
||||||
|
currentIndex = -1; // The first MoveNext() should bring the iterator to 0
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext() => ++currentIndex < reverseQueue.Count;
|
||||||
|
|
||||||
|
public void Reset() => currentIndex = -1;
|
||||||
|
|
||||||
|
public readonly T Current => reverseQueue[currentIndex];
|
||||||
|
|
||||||
|
readonly object IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
reverseQueue = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,18 +28,6 @@ namespace osu.Game.Rulesets.Judgements
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05;
|
protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this <see cref="Judgement"/> should affect the current combo.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328
|
|
||||||
public virtual bool AffectsCombo => true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this <see cref="Judgement"/> should be counted as base (combo) or bonus score.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328
|
|
||||||
public virtual bool IsBonus => !AffectsCombo;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum <see cref="HitResult"/> that can be achieved.
|
/// The maximum <see cref="HitResult"/> that can be achieved.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -11,7 +11,6 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions.TypeExtensions;
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
@ -736,24 +735,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
if (!Result.HasResult)
|
if (!Result.HasResult)
|
||||||
throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}.");
|
throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}.");
|
||||||
|
|
||||||
// Some (especially older) rulesets use scorable judgements instead of the newer ignorehit/ignoremiss judgements.
|
|
||||||
// Can be removed 20210328
|
|
||||||
if (Result.Judgement.MaxResult == HitResult.IgnoreHit)
|
|
||||||
{
|
|
||||||
HitResult originalType = Result.Type;
|
|
||||||
|
|
||||||
if (Result.Type == HitResult.Miss)
|
|
||||||
Result.Type = HitResult.IgnoreMiss;
|
|
||||||
else if (Result.Type >= HitResult.Meh && Result.Type <= HitResult.Perfect)
|
|
||||||
Result.Type = HitResult.IgnoreHit;
|
|
||||||
|
|
||||||
if (Result.Type != originalType)
|
|
||||||
{
|
|
||||||
Logger.Log($"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n"
|
|
||||||
+ $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2021-03-28 onwards.", level: LogLevel.Important);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult))
|
if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
|
@ -139,15 +139,6 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected virtual void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
{
|
|
||||||
// ReSharper disable once MethodSupportsCancellation (https://youtrack.jetbrains.com/issue/RIDER-44520)
|
|
||||||
#pragma warning disable 618
|
|
||||||
CreateNestedHitObjects();
|
|
||||||
#pragma warning restore 618
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318
|
|
||||||
protected virtual void CreateNestedHitObjects()
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +156,39 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
return interpolateVertices(indexOfDistance(d), d);
|
return interpolateVertices(indexOfDistance(d), d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the control points belonging to the same segment as the one given.
|
||||||
|
/// The first point has a PathType which all other points inherit.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="controlPoint">One of the control points in the segment.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public List<PathControlPoint> PointsInSegment(PathControlPoint controlPoint)
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
List<PathControlPoint> pointsInCurrentSegment = new List<PathControlPoint>();
|
||||||
|
|
||||||
|
foreach (PathControlPoint point in ControlPoints)
|
||||||
|
{
|
||||||
|
if (point.Type.Value != null)
|
||||||
|
{
|
||||||
|
if (!found)
|
||||||
|
pointsInCurrentSegment.Clear();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pointsInCurrentSegment.Add(point);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pointsInCurrentSegment.Add(point);
|
||||||
|
|
||||||
|
if (point == controlPoint)
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pointsInCurrentSegment;
|
||||||
|
}
|
||||||
|
|
||||||
private void invalidate()
|
private void invalidate()
|
||||||
{
|
{
|
||||||
pathCache.Invalidate();
|
pathCache.Invalidate();
|
||||||
|
@ -346,12 +346,6 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
|
|
||||||
score.HitEvents = hitEvents;
|
score.HitEvents = hitEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a <see cref="HitWindows"/> for this processor.
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Method is now unused.")] // Can be removed 20210328
|
|
||||||
public virtual HitWindows CreateHitWindows() => new HitWindows();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ScoringMode
|
public enum ScoringMode
|
||||||
|
@ -8,21 +8,28 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||||
{
|
{
|
||||||
public class MultiplayerMatchFooter : CompositeDrawable
|
public class MultiplayerMatchFooter : CompositeDrawable
|
||||||
{
|
{
|
||||||
public const float HEIGHT = 50;
|
public const float HEIGHT = 50;
|
||||||
|
private const float ready_button_width = 600;
|
||||||
|
private const float spectate_button_width = 200;
|
||||||
|
|
||||||
public Action OnReadyClick
|
public Action OnReadyClick
|
||||||
{
|
{
|
||||||
set => readyButton.OnReadyClick = value;
|
set => readyButton.OnReadyClick = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Action OnSpectateClick
|
||||||
|
{
|
||||||
|
set => spectateButton.OnSpectateClick = value;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly Drawable background;
|
private readonly Drawable background;
|
||||||
private readonly MultiplayerReadyButton readyButton;
|
private readonly MultiplayerReadyButton readyButton;
|
||||||
|
private readonly MultiplayerSpectateButton spectateButton;
|
||||||
|
|
||||||
public MultiplayerMatchFooter()
|
public MultiplayerMatchFooter()
|
||||||
{
|
{
|
||||||
@ -32,11 +39,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
background = new Box { RelativeSizeAxes = Axes.Both },
|
background = new Box { RelativeSizeAxes = Axes.Both },
|
||||||
|
new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
null,
|
||||||
|
spectateButton = new MultiplayerSpectateButton
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
null,
|
||||||
readyButton = new MultiplayerReadyButton
|
readyButton = new MultiplayerReadyButton
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Origin = Anchor.Centre,
|
},
|
||||||
Size = new Vector2(600, 50),
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(),
|
||||||
|
new Dimension(maxSize: spectate_button_width),
|
||||||
|
new Dimension(GridSizeMode.Absolute, 10),
|
||||||
|
new Dimension(maxSize: ready_button_width),
|
||||||
|
new Dimension()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -78,8 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
Debug.Assert(Room != null);
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||||
|
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||||
|
|
||||||
string countText = $"({newCountReady} / {Room.Users.Count} ready)";
|
string countText = $"({newCountReady} / {newCountTotal} ready)";
|
||||||
|
|
||||||
switch (localUser.State)
|
switch (localUser.State)
|
||||||
{
|
{
|
||||||
@ -88,6 +89,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
updateButtonColour(true);
|
updateButtonColour(true);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Spectating:
|
||||||
case MultiplayerUserState.Ready:
|
case MultiplayerUserState.Ready:
|
||||||
if (Room?.Host?.Equals(localUser) == true)
|
if (Room?.Host?.Equals(localUser) == true)
|
||||||
{
|
{
|
||||||
@ -103,7 +105,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
|
bool enableButton = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
|
||||||
|
|
||||||
|
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
|
||||||
|
if (localUser.State == MultiplayerUserState.Spectating)
|
||||||
|
enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0;
|
||||||
|
|
||||||
|
button.Enabled.Value = enableButton;
|
||||||
|
|
||||||
if (newCountReady != countReady)
|
if (newCountReady != countReady)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Backgrounds;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||||
|
{
|
||||||
|
public class MultiplayerSpectateButton : MultiplayerRoomComposite
|
||||||
|
{
|
||||||
|
public Action OnSpectateClick
|
||||||
|
{
|
||||||
|
set => button.Action = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
private IBindable<bool> operationInProgress;
|
||||||
|
|
||||||
|
private readonly ButtonWithTrianglesExposed button;
|
||||||
|
|
||||||
|
public MultiplayerSpectateButton()
|
||||||
|
{
|
||||||
|
InternalChild = button = new ButtonWithTrianglesExposed
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Size = Vector2.One,
|
||||||
|
Enabled = { Value = true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
|
||||||
|
operationInProgress.BindValueChanged(_ => updateState());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnRoomUpdated()
|
||||||
|
{
|
||||||
|
base.OnRoomUpdated();
|
||||||
|
|
||||||
|
updateState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
var localUser = Client.LocalUser;
|
||||||
|
|
||||||
|
if (localUser == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
switch (localUser.State)
|
||||||
|
{
|
||||||
|
default:
|
||||||
|
button.Text = "Spectate";
|
||||||
|
button.BackgroundColour = colours.BlueDark;
|
||||||
|
button.Triangles.ColourDark = colours.BlueDarker;
|
||||||
|
button.Triangles.ColourLight = colours.Blue;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Spectating:
|
||||||
|
button.Text = "Stop spectating";
|
||||||
|
button.BackgroundColour = colours.Gray4;
|
||||||
|
button.Triangles.ColourDark = colours.Gray5;
|
||||||
|
button.Triangles.ColourLight = colours.Gray6;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ButtonWithTrianglesExposed : TriangleButton
|
||||||
|
{
|
||||||
|
public new Triangles Triangles => base.Triangles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -221,7 +221,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
{
|
{
|
||||||
new MultiplayerMatchFooter
|
new MultiplayerMatchFooter
|
||||||
{
|
{
|
||||||
OnReadyClick = onReadyClick
|
OnReadyClick = onReadyClick,
|
||||||
|
OnSpectateClick = onSpectateClick
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -363,7 +364,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
Debug.Assert(readyClickOperation == null);
|
Debug.Assert(readyClickOperation == null);
|
||||||
readyClickOperation = ongoingOperationTracker.BeginOperation();
|
readyClickOperation = ongoingOperationTracker.BeginOperation();
|
||||||
|
|
||||||
if (client.IsHost && client.LocalUser?.State == MultiplayerUserState.Ready)
|
if (client.IsHost && (client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating))
|
||||||
{
|
{
|
||||||
client.StartMatch()
|
client.StartMatch()
|
||||||
.ContinueWith(t =>
|
.ContinueWith(t =>
|
||||||
@ -390,6 +391,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onSpectateClick()
|
||||||
|
{
|
||||||
|
Debug.Assert(readyClickOperation == null);
|
||||||
|
readyClickOperation = ongoingOperationTracker.BeginOperation();
|
||||||
|
|
||||||
|
client.ToggleSpectate().ContinueWith(t => endOperation());
|
||||||
|
|
||||||
|
void endOperation()
|
||||||
|
{
|
||||||
|
readyClickOperation?.Dispose();
|
||||||
|
readyClickOperation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void onRoomUpdated()
|
private void onRoomUpdated()
|
||||||
{
|
{
|
||||||
// user mods may have changed.
|
// user mods may have changed.
|
||||||
|
@ -135,6 +135,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
|||||||
icon.Colour = colours.BlueLighter;
|
icon.Colour = colours.BlueLighter;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case MultiplayerUserState.Spectating:
|
||||||
|
text.Text = "spectating";
|
||||||
|
icon.Icon = FontAwesome.Solid.Binoculars;
|
||||||
|
icon.Colour = colours.BlueLight;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(state), state, null);
|
throw new ArgumentOutOfRangeException(nameof(state), state, null);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,10 @@ namespace osu.Game.Screens.Play
|
|||||||
if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId))
|
if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new CreateSoloScoreRequest(beatmapId, Game.VersionHash);
|
if (!(Ruleset.Value.ID is int rulesetId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new CreateSoloScoreRequest(beatmapId, rulesetId, Game.VersionHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
|
protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
|
||||||
|
@ -152,7 +152,7 @@ namespace osu.Game.Screens.Select.Carousel
|
|||||||
{
|
{
|
||||||
if (sampleHover == null) return;
|
if (sampleHover == null) return;
|
||||||
|
|
||||||
sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2);
|
sampleHover.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
|
||||||
sampleHover.Play();
|
sampleHover.Play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ using osu.Game.Graphics.Sprites;
|
|||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select
|
namespace osu.Game.Screens.Select
|
||||||
{
|
{
|
||||||
@ -65,6 +66,7 @@ namespace osu.Game.Screens.Select
|
|||||||
private readonly Box light;
|
private readonly Box light;
|
||||||
|
|
||||||
public FooterButton()
|
public FooterButton()
|
||||||
|
: base(HoverSampleSet.SongSelect)
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
Shear = SHEAR;
|
Shear = SHEAR;
|
||||||
|
@ -58,6 +58,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ChangeRoomState(MultiplayerRoomState newState)
|
||||||
|
{
|
||||||
|
Debug.Assert(Room != null);
|
||||||
|
((IMultiplayerClient)this).RoomStateChanged(newState);
|
||||||
|
}
|
||||||
|
|
||||||
public void ChangeUserState(int userId, MultiplayerUserState newState)
|
public void ChangeUserState(int userId, MultiplayerUserState newState)
|
||||||
{
|
{
|
||||||
Debug.Assert(Room != null);
|
Debug.Assert(Room != null);
|
||||||
@ -71,6 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
case MultiplayerUserState.Loaded:
|
case MultiplayerUserState.Loaded:
|
||||||
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
|
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
|
||||||
{
|
{
|
||||||
|
ChangeRoomState(MultiplayerRoomState.Playing);
|
||||||
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
|
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
|
||||||
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
|
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
|
||||||
|
|
||||||
@ -82,6 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
case MultiplayerUserState.FinishedPlay:
|
case MultiplayerUserState.FinishedPlay:
|
||||||
if (Room.Users.All(u => u.State != MultiplayerUserState.Playing))
|
if (Room.Users.All(u => u.State != MultiplayerUserState.Playing))
|
||||||
{
|
{
|
||||||
|
ChangeRoomState(MultiplayerRoomState.Open);
|
||||||
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
|
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
|
||||||
ChangeUserState(u.UserID, MultiplayerUserState.Results);
|
ChangeUserState(u.UserID, MultiplayerUserState.Results);
|
||||||
|
|
||||||
@ -173,6 +181,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
Debug.Assert(Room != null);
|
Debug.Assert(Room != null);
|
||||||
|
|
||||||
|
ChangeRoomState(MultiplayerRoomState.WaitingForLoad);
|
||||||
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
||||||
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
|
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
if (CreateNestedActionContainer)
|
if (CreateNestedActionContainer)
|
||||||
{
|
{
|
||||||
mainContent = new GlobalActionContainer(null, null).WithChild(mainContent);
|
mainContent = new GlobalActionContainer(null).WithChild(mainContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
base.Content.AddRange(new Drawable[]
|
base.Content.AddRange(new Drawable[]
|
||||||
|
@ -29,8 +29,8 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
|
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.407.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.410.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.2.0" />
|
<PackageReference Include="Sentry" Version="3.2.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||||
|
@ -70,8 +70,8 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.407.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.410.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@ -93,7 +93,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.407.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.410.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user