1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 03:25:11 +08:00

Merge branch 'master' into multiplayer-spectator-screen

This commit is contained in:
smoogipoo 2021-04-08 21:13:37 +09:00
commit 125358158b
19 changed files with 420 additions and 63 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.402.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.407.0" />
</ItemGroup>
</Project>

View File

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

View File

@ -276,6 +276,104 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
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 addClickStep(MouseButton button)

View File

@ -2,14 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
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 Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
public List<PathControlPoint> PointsInSegment;
public readonly BindableBool IsSelected = new BindableBool();
public readonly PathControlPoint ControlPoint;
@ -54,6 +59,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
this.slider = slider;
ControlPoint = controlPoint;
slider.Path.Version.BindValueChanged(_ =>
{
PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
updatePathType();
}, runOnceImmediately: true);
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
Origin = Anchor.Centre;
@ -150,6 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnClick(ClickEvent e) => RequestSelection != null;
private Vector2 dragStartPosition;
private PathType? dragPathType;
protected override bool OnDragStart(DragStartEvent e)
{
@ -159,6 +171,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (e.Button == MouseButton.Left)
{
dragStartPosition = ControlPoint.Position.Value;
dragPathType = PointsInSegment[0].Type.Value;
changeHandler?.BeginChange();
return true;
}
@ -184,10 +198,30 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
else
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();
/// <summary>
/// Handles correction of invalid path types.
/// </summary>
private void updatePathType()
{
if (ControlPoint.Type.Value != PathType.PerfectCurve)
return;
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position.Value).ToArray();
if (points.Length != 3)
return;
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
ControlPoint.Type.Value = PathType.Bezier;
}
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>

View File

@ -142,6 +142,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.Update();
updateSlider();
// Maintain the path type in case it got defaulted to bezier at some point during the drag.
updatePathType();
}
private void updatePathType()

View File

@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.OnOperationEnded();
referenceOrigin = null;
referencePathTypes = null;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent)
@ -53,6 +54,12 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
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()
{
var hitObjects = EditorBeatmap.SelectedHitObjects;
@ -194,6 +201,8 @@ namespace osu.Game.Rulesets.Osu.Edit
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));
// 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;
}
// 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.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);

View File

@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[BackgroundDependencyLoader]
private void load(OsuGameBase game)
{
Child = globalActionContainer = new GlobalActionContainer(game, null);
Child = globalActionContainer = new GlobalActionContainer(game);
}
[SetUp]

View File

@ -209,9 +209,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
addClickButtonStep();
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("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);
}
}

View File

@ -44,6 +44,20 @@ namespace osu.Game.Tests.Visual.Navigation
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]
public void TestRetryCountIncrements()
{

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
@ -13,20 +12,23 @@ namespace osu.Game.Input.Bindings
{
public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput
{
[CanBeNull]
private readonly GlobalInputManager globalInputManager;
private readonly Drawable handler;
private InputManager parentInputManager;
public GlobalActionContainer(OsuGameBase game, [CanBeNull] GlobalInputManager globalInputManager)
public GlobalActionContainer(OsuGameBase game)
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
{
this.globalInputManager = globalInputManager;
if (game is IKeyBindingHandler<GlobalAction>)
handler = game;
}
protected override void LoadComplete()
{
base.LoadComplete();
parentInputManager = GetContainingInputManager();
}
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
.Concat(EditorKeyBindings)
.Concat(InGameKeyBindings)
@ -113,7 +115,12 @@ namespace osu.Game.Input.Bindings
{
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;
}

View File

@ -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)
};
}
}
}

View File

@ -308,18 +308,21 @@ namespace osu.Game
AddInternal(RulesetConfigCache);
var globalInput = new GlobalInputManager(this)
GlobalActionContainer globalBindings;
var mainContent = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Child = MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }
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 };
base.Content.Add(CreateScalingContainer().WithChild(globalInput));
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
KeyBindingStore.Register(globalInput.GlobalBindings);
dependencies.Cache(globalInput.GlobalBindings);
KeyBindingStore.Register(globalBindings);
dependencies.Cache(globalBindings);
PreviewTrackManager previewTrackManager;
dependencies.Cache(previewTrackManager = new PreviewTrackManager());

View File

@ -156,6 +156,39 @@ namespace osu.Game.Rulesets.Objects
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()
{
pathCache.Invalidate();

View File

@ -105,14 +105,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
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.
// 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)
{
button.Enabled.Value &= Room?.Host?.Equals(localUser) == true;
button.Enabled.Value &= newCountReady > 0;
}
enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0;
button.Enabled.Value = enableButton;
if (newCountReady != countReady)
{

View File

@ -68,16 +68,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
default:
button.Text = "Spectate";
button.BackgroundColour = colours.Blue;
button.Triangles.ColourDark = colours.Blue;
button.Triangles.ColourLight = colours.BlueLight;
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.Red;
button.Triangles.ColourDark = colours.Red;
button.Triangles.ColourLight = colours.RedLight;
button.BackgroundColour = colours.Gray4;
button.Triangles.ColourDark = colours.Gray5;
button.Triangles.ColourLight = colours.Gray6;
break;
}

View File

@ -137,7 +137,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
case MultiplayerUserState.Spectating:
text.Text = "spectating";
icon.Icon = FontAwesome.Solid.Eye;
icon.Icon = FontAwesome.Solid.Binoculars;
icon.Colour = colours.BlueLight;
break;

View File

@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual
if (CreateNestedActionContainer)
{
mainContent = new GlobalActionContainer(null, null).WithChild(mainContent);
mainContent = new GlobalActionContainer(null).WithChild(mainContent);
}
base.Content.AddRange(new Drawable[]

View File

@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.402.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.407.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
<PackageReference Include="Sentry" Version="3.2.0" />
<PackageReference Include="SharpCompress" Version="0.28.1" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.402.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.407.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.402.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.407.0" />
<PackageReference Include="SharpCompress" Version="0.28.1" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />