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

Merge branch 'master' into fix-limit-distance-snap-to-current

This commit is contained in:
Bartłomiej Dach 2025-02-03 08:57:27 +01:00 committed by GitHub
commit ee7d281e73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 837 additions and 697 deletions

View File

@ -1,34 +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 Android.Content.PM;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play;
namespace osu.Android
{
public partial class GameplayScreenRotationLocker : Component
{
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
[Resolved]
private OsuGameActivity gameActivity { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(ILocalUserPlayInfo localUserPlayInfo)
{
localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
localUserPlaying.BindValueChanged(updateLock, true);
}
private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
{
gameActivity.RunOnUiThread(() =>
{
gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
});
}
}
}

View File

@ -49,6 +49,8 @@ namespace osu.Android
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks> /// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
public new bool IsTablet { get; private set; }
private readonly OsuGameAndroid game; private readonly OsuGameAndroid game;
private bool gameCreated; private bool gameCreated;
@ -89,9 +91,9 @@ namespace osu.Android
WindowManager.DefaultDisplay.GetSize(displaySize); WindowManager.DefaultDisplay.GetSize(displaySize);
#pragma warning restore CA1422 #pragma warning restore CA1422
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
bool isTablet = smallestWidthDp >= 600f; IsTablet = smallestWidthDp >= 600f;
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
// Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android. // Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
// The assembly files are not available as files either after native AOT. // The assembly files are not available as files either after native AOT.

View File

@ -3,11 +3,13 @@
using System; using System;
using Android.App; using Android.App;
using Android.Content.PM;
using Microsoft.Maui.Devices; using Microsoft.Maui.Devices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.Screens;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
@ -71,7 +73,35 @@ namespace osu.Android
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
LoadComponentAsync(new GameplayScreenRotationLocker(), Add); UserPlayingState.BindValueChanged(_ => updateOrientation());
}
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
{
base.ScreenChanged(current, newScreen);
if (newScreen != null)
updateOrientation();
}
private void updateOrientation()
{
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet);
switch (orientation)
{
case MobileUtils.Orientation.Locked:
gameActivity.RequestedOrientation = ScreenOrientation.Locked;
break;
case MobileUtils.Orientation.Portrait:
gameActivity.RequestedOrientation = ScreenOrientation.Portrait;
break;
case MobileUtils.Orientation.Default:
gameActivity.RequestedOrientation = gameActivity.DefaultOrientation;
break;
}
} }
public override void SetHost(GameHost host) public override void SetHost(GameHost host)

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
}); });
} }
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
}); });
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
}); });
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
}); });
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
CreateBeatmap = () => new Beatmap CreateBeatmap = () => new Beatmap
{ {
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(), HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),

View File

@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new ColumnHitObjectArea(new HitObjectContainer()) Child = new ColumnHitObjectArea
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both,
Child = new HitObjectContainer(),
} }
}, },
new ColumnTestContainer(1, ManiaAction.Key2) new ColumnTestContainer(1, ManiaAction.Key2)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new ColumnHitObjectArea(new HitObjectContainer()) Child = new ColumnHitObjectArea
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both,
Child = new HitObjectContainer(),
} }
} }
} }

View File

@ -0,0 +1,68 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaTouchInput : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestTouchInput()
{
for (int i = 0; i < 4; i++)
{
int index = i;
AddStep($"touch column {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(index).Action.Value));
AddStep($"release column {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action released",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Not.Contain(getColumn(index).Action.Value));
}
}
[Test]
public void TestOneColumnMultipleTouches()
{
AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(0).Action.Value));
AddStep("touch another finger", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action still pressed",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(0).Action.Value));
AddStep("release first finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action still pressed",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(0).Action.Value));
AddStep("release second finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action released",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Not.Contain(getColumn(0).Action.Value));
}
private Column getColumn(int index) => this.ChildrenOfType<Column>().ElementAt(index);
}
}

View File

@ -1,49 +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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaTouchInputArea : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestTouchAreaNotInitiallyVisible()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
}
[Test]
public void TestPressReceptors()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
for (int i = 0; i < 4; i++)
{
int index = i;
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getReceptor(index).Action.Value));
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
}
}
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
}
}

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania namespace osu.Game.Rulesets.Mania
{ {
[Cached] // Used for touch input, see ColumnTouchInputArea. [Cached] // Used for touch input, see Column.OnTouchDown/OnTouchUp.
public partial class ManiaInputManager : RulesetInputManager<ManiaAction> public partial class ManiaInputManager : RulesetInputManager<ManiaAction>
{ {
public ManiaInputManager(RulesetInfo ruleset, int variant) public ManiaInputManager(RulesetInfo ruleset, int variant)

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
{ {
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; HitObjectContainer hoc = column.HitObjectContainer;
Container hocParent = (Container)hoc.Parent!; Container hocParent = (Container)hoc.Parent!;
hocParent.Remove(hoc, false); hocParent.Remove(hoc, false);

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{ {
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
{ {
private const float judgement_y_position = 160; private const float judgement_y_position = -180f;
private RingExplosion? ringExplosion; private RingExplosion? ringExplosion;

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -23,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
this.result = result; this.result = result;
this.animation = animation; this.animation = animation;
Anchor = Anchor.Centre; Anchor = Anchor.BottomCentre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -32,12 +31,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load(ISkinSource skin)
{ {
float? scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value; float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
if (scorePosition != null) float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
scorePosition -= Stage.HIT_TARGET_POSITION + 150; Y = scorePosition - absoluteHitPosition;
Y = scorePosition ?? 0;
InternalChild = animation.With(d => InternalChild = animation.With(d =>
{ {

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
@ -45,11 +44,11 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }; internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
private DrawablePool<PoolableHitExplosion> hitExplosionPool; private DrawablePool<PoolableHitExplosion> hitExplosionPool = null!;
private readonly OrderedHitPolicy hitPolicy; private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements; public Container UnderlayElements => HitObjectArea.UnderlayElements;
private GameplaySampleTriggerSource sampleTriggerSource; private GameplaySampleTriggerSource sampleTriggerSource = null!;
/// <summary> /// <summary>
/// Whether this is a special (ie. scratch) column. /// Whether this is a special (ie. scratch) column.
@ -67,11 +66,15 @@ namespace osu.Game.Rulesets.Mania.UI
Width = COLUMN_WIDTH; Width = COLUMN_WIDTH;
hitPolicy = new OrderedHitPolicy(HitObjectContainer); hitPolicy = new OrderedHitPolicy(HitObjectContainer);
HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }; HitObjectArea = new ColumnHitObjectArea
{
RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer,
};
} }
[Resolved] [Resolved]
private ISkinSource skin { get; set; } private ISkinSource skin { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host) private void load(GameHost host)
@ -132,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.UI
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (skin != null) if (skin.IsNotNull())
skin.SourceChanged -= onSourceChanged; skin.SourceChanged -= onSourceChanged;
} }
@ -180,5 +183,29 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
#region Touch Input
[Resolved]
private ManiaInputManager? maniaInputManager { get; set; }
private int touchActivationCount;
protected override bool OnTouchDown(TouchDownEvent e)
{
maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
touchActivationCount++;
return true;
}
protected override void OnTouchUp(TouchUpEvent e)
{
touchActivationCount--;
if (touchActivationCount == 0)
maniaInputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
}
#endregion
} }
} }

View File

@ -3,13 +3,12 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI.Components namespace osu.Game.Rulesets.Mania.UI.Components
{ {
public partial class ColumnHitObjectArea : HitObjectArea public partial class ColumnHitObjectArea : HitPositionPaddedContainer
{ {
public readonly Container Explosions; public readonly Container Explosions;
@ -17,25 +16,29 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private readonly Drawable hitTarget; private readonly Drawable hitTarget;
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) protected override Container<Drawable> Content => content;
: base(hitObjectContainer)
private readonly Container content;
public ColumnHitObjectArea()
{ {
AddRangeInternal(new[] AddRangeInternal(new[]
{ {
UnderlayElements = new Container UnderlayElements = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Depth = 2,
}, },
hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget()) hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Depth = 1 },
content = new Container
{
RelativeSizeAxes = Axes.Both,
}, },
Explosions = new Container Explosions = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Depth = -1,
} }
}); });
} }

View File

@ -1,52 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI.Components namespace osu.Game.Rulesets.Mania.UI.Components
{ {
public partial class HitObjectArea : SkinReloadableDrawable public partial class HitPositionPaddedContainer : Container
{ {
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>(); protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
public readonly HitObjectContainer HitObjectContainer;
public HitObjectArea(HitObjectContainer hitObjectContainer) [Resolved]
{ private ISkinSource skin { get; set; } = null!;
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer = hitObjectContainer
};
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo) private void load(IScrollingInfo scrollingInfo)
{ {
Direction.BindTo(scrollingInfo.Direction); Direction.BindTo(scrollingInfo.Direction);
Direction.BindValueChanged(onDirectionChanged, true); Direction.BindValueChanged(_ => UpdateHitPosition(), true);
skin.SourceChanged += onSkinChanged;
} }
protected override void SkinChanged(ISkinSource skin) private void onSkinChanged() => UpdateHitPosition();
{
base.SkinChanged(skin);
UpdateHitPosition();
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
UpdateHitPosition();
}
protected virtual void UpdateHitPosition() protected virtual void UpdateHitPosition()
{ {
float hitPosition = CurrentSkin.GetConfig<ManiaSkinConfigurationLookup, float>( float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? Stage.HIT_TARGET_POSITION; ?? Stage.HIT_TARGET_POSITION;
@ -54,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
? new MarginPadding { Top = hitPosition } ? new MarginPadding { Top = hitPosition }
: new MarginPadding { Bottom = hitPosition }; : new MarginPadding { Bottom = hitPosition };
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skin.IsNotNull())
skin.SourceChanged -= onSkinChanged;
}
} }
} }

View File

@ -6,6 +6,7 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
@ -15,9 +16,12 @@ namespace osu.Game.Rulesets.Mania.UI
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
{ {
private const float judgement_y_position = -180f;
public DefaultManiaJudgementPiece(HitResult result) public DefaultManiaJudgementPiece(HitResult result)
: base(result) : base(result)
{ {
Y = judgement_y_position;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -32,8 +36,20 @@ namespace osu.Game.Rulesets.Mania.UI
switch (Result) switch (Result)
{ {
case HitResult.None: case HitResult.None:
this.FadeOutFromOne(800);
break;
case HitResult.Miss: case HitResult.Miss:
base.PlayAnimation(); this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
this.FadeOutFromOne(800);
break; break;
default: default:
@ -43,8 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI
this.Delay(50) this.Delay(50)
.ScaleTo(0.75f, 250) .ScaleTo(0.75f, 250)
.FadeOut(200); .FadeOut(200);
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
break; break;
} }
} }

View File

@ -32,7 +32,6 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
[Cached]
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject> public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
{ {
/// <summary> /// <summary>
@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable<BarLine> BarLines; public IEnumerable<BarLine> BarLines;
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1;
protected override bool RelativeScaleBeatLengths => true; protected override bool RelativeScaleBeatLengths => true;
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
@ -110,8 +111,6 @@ namespace osu.Game.Rulesets.Mania.UI
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
KeyBindingInputManager.Add(new ManiaTouchInputArea());
} }
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
@ -162,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// <returns>The scroll time.</returns> /// <returns>The scroll time.</returns>
public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(this);
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages); protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);

View File

@ -1,17 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{ {
public ManiaPlayfieldAdjustmentContainer() protected override Container<Drawable> Content { get; }
private readonly DrawSizePreservingFillContainer scalingContainer;
private readonly DrawableManiaRuleset drawableManiaRuleset;
public ManiaPlayfieldAdjustmentContainer(DrawableManiaRuleset drawableManiaRuleset)
{ {
Anchor = Anchor.Centre; this.drawableManiaRuleset = drawableManiaRuleset;
Origin = Anchor.Centre; InternalChild = scalingContainer = new DrawSizePreservingFillContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Child = Content = new Container
{
RelativeSizeAxes = Axes.Both,
}
};
}
protected override void Update()
{
base.Update();
float aspectRatio = DrawWidth / DrawHeight;
bool isPortrait = aspectRatio < 1f;
if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1)
{
// Scale playfield up by 25% to become playable on mobile devices,
// and leave a 10% horizontal gap if the playfield is scaled down due to being too wide.
const float base_scale = 1.25f;
const float base_width = 768f / base_scale;
const float side_gap = 0.9f;
scalingContainer.Strategy = DrawSizePreservationStrategy.Maximum;
float stageWidth = drawableManiaRuleset.Playfield.Stages[0].DrawWidth;
scalingContainer.TargetDrawSize = new Vector2(1024, base_width * Math.Max(stageWidth / aspectRatio / (base_width * side_gap), 1f));
}
else
{
scalingContainer.Strategy = DrawSizePreservationStrategy.Minimum;
scalingContainer.Scale = new Vector2(1f);
scalingContainer.Size = new Vector2(1f);
scalingContainer.TargetDrawSize = new Vector2(1024, 768);
}
} }
} }
} }

View File

@ -1,199 +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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
/// <summary>
/// An overlay that captures and displays osu!mania mouse and touch input.
/// </summary>
public partial class ManiaTouchInputArea : VisibilityContainer
{
// visibility state affects our child. we always want to handle input.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
[SettingSource("Spacing", "The spacing between receptors.")]
public BindableFloat Spacing { get; } = new BindableFloat(10)
{
Precision = 1,
MinValue = 0,
MaxValue = 100,
};
[SettingSource("Opacity", "The receptor opacity.")]
public BindableFloat Opacity { get; } = new BindableFloat(1)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 1
};
[Resolved]
private DrawableManiaRuleset drawableRuleset { get; set; } = null!;
private GridContainer gridContainer = null!;
public ManiaTouchInputArea()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
RelativeSizeAxes = Axes.Both;
Height = 0.5f;
}
[BackgroundDependencyLoader]
private void load()
{
List<Drawable> receptorGridContent = new List<Drawable>();
List<Dimension> receptorGridDimensions = new List<Dimension>();
bool first = true;
foreach (var stage in drawableRuleset.Playfield.Stages)
{
foreach (var column in stage.Columns)
{
if (!first)
{
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
}
receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } });
receptorGridDimensions.Add(new Dimension());
first = false;
}
}
InternalChild = gridContainer = new GridContainer
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Content = new[] { receptorGridContent.ToArray() },
ColumnDimensions = receptorGridDimensions.ToArray()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// Hide whenever the keyboard is used.
Hide();
return false;
}
protected override bool OnTouchDown(TouchDownEvent e)
{
Show();
return true;
}
protected override void PopIn()
{
gridContainer.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
gridContainer.FadeOut(300);
}
public partial class ColumnInputReceptor : CompositeDrawable
{
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
private readonly Box highlightOverlay;
[Resolved]
private ManiaInputManager? inputManager { get; set; }
private bool isPressed;
public ColumnInputReceptor()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.15f,
},
highlightOverlay = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
}
}
};
}
protected override bool OnTouchDown(TouchDownEvent e)
{
updateButton(true);
return false; // handled by parent container to show overlay.
}
protected override void OnTouchUp(TouchUpEvent e)
{
updateButton(false);
}
private void updateButton(bool press)
{
if (press == isPressed)
return;
isPressed = press;
if (press)
{
inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
}
else
{
inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
}
}
}
private partial class Gutter : Drawable
{
public readonly IBindable<float> Spacing = new Bindable<float>();
public Gutter()
{
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
}
}
}
}

View File

@ -103,12 +103,13 @@ namespace osu.Game.Rulesets.Mania.UI
Width = 1366, // Bar lines should only be masked on the vertical axis Width = 1366, // Bar lines should only be masked on the vertical axis
BypassAutoSizeAxes = Axes.Both, BypassAutoSizeAxes = Axes.Both,
Masking = true, Masking = true,
Child = barLineContainer = new HitObjectArea(HitObjectContainer) Child = barLineContainer = new HitPositionPaddedContainer
{ {
Name = "Bar lines", Name = "Bar lines",
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Child = HitObjectContainer,
} }
}, },
columnFlow = new ColumnFlow<Column>(definition) columnFlow = new ColumnFlow<Column>(definition)
@ -119,12 +120,13 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
judgements = new JudgementContainer<DrawableManiaJudgement> new HitPositionPaddedContainer
{ {
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Y = HIT_TARGET_POSITION + 150 Child = judgements = new JudgementContainer<DrawableManiaJudgement>
{
RelativeSizeAxes = Axes.Both,
},
}, },
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
} }
@ -218,7 +220,7 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
j.Apply(result, judgedObject); j.Apply(result, judgedObject);
j.Anchor = Anchor.Centre; j.Anchor = Anchor.BottomCentre;
j.Origin = Anchor.Centre; j.Origin = Anchor.Centre;
})!); })!);
} }

View File

@ -4,6 +4,7 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -25,13 +26,16 @@ namespace osu.Game.Tests.Visual.Editing
[Test] [Test]
public void TestLocallyModifyingOnlineBeatmap() public void TestLocallyModifyingOnlineBeatmap()
{ {
string initialHash = string.Empty;
AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0)); AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0));
AddStep("store hash for later", () => initialHash = EditorBeatmap.BeatmapInfo.MD5Hash);
AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0)); AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0));
SaveEditor(); SaveEditor();
ReloadEditorToSameBeatmap(); ReloadEditorToSameBeatmap();
AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1)); AddAssert("beatmap marked as locally modified", () => EditorBeatmap.BeatmapInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified));
AddAssert("beatmap hash changed", () => EditorBeatmap.BeatmapInfo.MD5Hash, () => Is.Not.EqualTo(initialHash));
} }
} }
} }

View File

@ -475,11 +475,8 @@ namespace osu.Game.Beatmaps
beatmapContent.BeatmapInfo = beatmapInfo; beatmapContent.BeatmapInfo = beatmapInfo;
// Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this. // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this.
// Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file,
// which influences the beatmap checksums.
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
beatmapInfo.ResetOnlineInfo();
Realm.Write(r => Realm.Write(r =>
{ {

View File

@ -178,9 +178,9 @@ namespace osu.Game
/// </summary> /// </summary>
private readonly IBindable<bool> backButtonVisibility = new Bindable<bool>(); private readonly IBindable<bool> backButtonVisibility = new Bindable<bool>();
IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => playingState; IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => UserPlayingState;
private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>(); protected readonly Bindable<LocalUserPlayingState> UserPlayingState = new Bindable<LocalUserPlayingState>();
protected OsuScreenStack ScreenStack; protected OsuScreenStack ScreenStack;
@ -306,7 +306,7 @@ namespace osu.Game
protected override UserInputManager CreateUserInputManager() protected override UserInputManager CreateUserInputManager()
{ {
var userInputManager = base.CreateUserInputManager(); var userInputManager = base.CreateUserInputManager();
(userInputManager as OsuUserInputManager)?.PlayingState.BindTo(playingState); (userInputManager as OsuUserInputManager)?.PlayingState.BindTo(UserPlayingState);
return userInputManager; return userInputManager;
} }
@ -407,7 +407,7 @@ namespace osu.Game
// Transfer any runtime changes back to configuration file. // Transfer any runtime changes back to configuration file.
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString(); SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
playingState.BindValueChanged(p => UserPlayingState.BindValueChanged(p =>
{ {
BeatmapManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; BeatmapManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying;
SkinManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; SkinManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying;
@ -1548,7 +1548,7 @@ namespace osu.Game
GlobalCursorDisplay.ShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false; GlobalCursorDisplay.ShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
} }
private void screenChanged(IScreen current, IScreen newScreen) protected virtual void ScreenChanged([CanBeNull] IOsuScreen current, [CanBeNull] IOsuScreen newScreen)
{ {
SentrySdk.ConfigureScope(scope => SentrySdk.ConfigureScope(scope =>
{ {
@ -1564,10 +1564,10 @@ namespace osu.Game
switch (current) switch (current)
{ {
case Player player: case Player player:
player.PlayingState.UnbindFrom(playingState); player.PlayingState.UnbindFrom(UserPlayingState);
// reset for sanity. // reset for sanity.
playingState.Value = LocalUserPlayingState.NotPlaying; UserPlayingState.Value = LocalUserPlayingState.NotPlaying;
break; break;
} }
@ -1584,7 +1584,7 @@ namespace osu.Game
break; break;
case Player player: case Player player:
player.PlayingState.BindTo(playingState); player.PlayingState.BindTo(UserPlayingState);
break; break;
default: default:
@ -1592,30 +1592,32 @@ namespace osu.Game
break; break;
} }
if (current is IOsuScreen currentOsuScreen) if (current != null)
{ {
backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); backButtonVisibility.UnbindFrom(current.BackButtonVisibility);
OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); OverlayActivationMode.UnbindFrom(current.OverlayActivationMode);
configUserActivity.UnbindFrom(currentOsuScreen.Activity); configUserActivity.UnbindFrom(current.Activity);
} }
if (newScreen is IOsuScreen newOsuScreen) // Bind to new screen.
if (newScreen != null)
{ {
backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); backButtonVisibility.BindTo(newScreen.BackButtonVisibility);
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); OverlayActivationMode.BindTo(newScreen.OverlayActivationMode);
configUserActivity.BindTo(newOsuScreen.Activity); configUserActivity.BindTo(newScreen.Activity);
GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; // Handle various configuration updates based on new screen settings.
GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newScreen.HideMenuCursorOnNonMouseInput;
if (newOsuScreen.HideOverlaysOnEnter) if (newScreen.HideOverlaysOnEnter)
CloseAllOverlays(); CloseAllOverlays();
else else
Toolbar.Show(); Toolbar.Show();
if (newOsuScreen.ShowFooter) if (newScreen.ShowFooter)
{ {
BackButton.Hide(); BackButton.Hide();
ScreenFooter.SetButtons(newOsuScreen.CreateFooterButtons()); ScreenFooter.SetButtons(newScreen.CreateFooterButtons());
ScreenFooter.Show(); ScreenFooter.Show();
} }
else else
@ -1623,16 +1625,16 @@ namespace osu.Game
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>()); ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
ScreenFooter.Hide(); ScreenFooter.Hide();
} }
}
skinEditor.SetTarget((OsuScreen)newScreen); skinEditor.SetTarget((OsuScreen)newScreen);
} }
}
private void screenPushed(IScreen lastScreen, IScreen newScreen) => screenChanged(lastScreen, newScreen); private void screenPushed(IScreen lastScreen, IScreen newScreen) => ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen);
private void screenExited(IScreen lastScreen, IScreen newScreen) private void screenExited(IScreen lastScreen, IScreen newScreen)
{ {
screenChanged(lastScreen, newScreen); ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen);
if (newScreen == null) if (newScreen == null)
Exit(); Exit();

View File

@ -21,7 +21,7 @@ using Realms;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
[LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))] [LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))]
public partial class ScreenBeatmaps : FirstRunSetupScreen public partial class ScreenBeatmaps : WizardScreen
{ {
private ProgressRoundedButton downloadBundledButton = null!; private ProgressRoundedButton downloadBundledButton = null!;
private ProgressRoundedButton downloadTutorialButton = null!; private ProgressRoundedButton downloadTutorialButton = null!;

View File

@ -20,7 +20,7 @@ using osu.Game.Overlays.Settings.Sections;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
[LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))] [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))]
public partial class ScreenBehaviour : FirstRunSetupScreen public partial class ScreenBehaviour : WizardScreen
{ {
private SearchContainer<SettingsSection> searchContainer; private SearchContainer<SettingsSection> searchContainer;

View File

@ -31,7 +31,7 @@ using osuTK;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
[LocalisableDescription(typeof(FirstRunOverlayImportFromStableScreenStrings), nameof(FirstRunOverlayImportFromStableScreenStrings.Header))] [LocalisableDescription(typeof(FirstRunOverlayImportFromStableScreenStrings), nameof(FirstRunOverlayImportFromStableScreenStrings.Header))]
public partial class ScreenImportFromStable : FirstRunSetupScreen public partial class ScreenImportFromStable : WizardScreen
{ {
private static readonly Vector2 button_size = new Vector2(400, 50); private static readonly Vector2 button_size = new Vector2(400, 50);

View File

@ -32,7 +32,7 @@ using osuTK;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
[LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))] [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))]
public partial class ScreenUIScale : FirstRunSetupScreen public partial class ScreenUIScale : WizardScreen
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)

View File

@ -23,7 +23,7 @@ using osuTK;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
[LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.WelcomeTitle))] [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.WelcomeTitle))]
public partial class ScreenWelcome : FirstRunSetupScreen public partial class ScreenWelcome : WizardScreen
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkConfigManager frameworkConfig) private void load(FrameworkConfigManager frameworkConfig)

View File

@ -1,38 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays.FirstRunSetup; using osu.Game.Overlays.FirstRunSetup;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
[Cached] [Cached]
public partial class FirstRunSetupOverlay : ShearedOverlayContainer public partial class FirstRunSetupOverlay : WizardOverlay
{ {
[Resolved] [Resolved]
private IPerformFromScreenRunner performer { get; set; } = null!; private IPerformFromScreenRunner performer { get; set; } = null!;
@ -43,28 +27,8 @@ namespace osu.Game.Overlays
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
private ScreenStack? stack;
public ShearedButton? NextButton => DisplayedFooterContent?.NextButton;
private readonly Bindable<bool> showFirstRunSetup = new Bindable<bool>(); private readonly Bindable<bool> showFirstRunSetup = new Bindable<bool>();
private int? currentStepIndex;
/// <summary>
/// The currently displayed screen, if any.
/// </summary>
public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen;
private readonly List<Type> steps = new List<Type>();
private Container screenContent = null!;
private Container content = null!;
private LoadingSpinner loading = null!;
private ScheduledDelegate? loadingShowDelegate;
public FirstRunSetupOverlay() public FirstRunSetupOverlay()
: base(OverlayColourScheme.Purple) : base(OverlayColourScheme.Purple)
{ {
@ -73,67 +37,15 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuColour colours, LegacyImportManager? legacyImportManager) private void load(OsuColour colours, LegacyImportManager? legacyImportManager)
{ {
steps.Add(typeof(ScreenWelcome)); AddStep<ScreenWelcome>();
steps.Add(typeof(ScreenUIScale)); AddStep<ScreenUIScale>();
steps.Add(typeof(ScreenBeatmaps)); AddStep<ScreenBeatmaps>();
if (legacyImportManager?.SupportsImportFromStable == true) if (legacyImportManager?.SupportsImportFromStable == true)
steps.Add(typeof(ScreenImportFromStable)); AddStep<ScreenImportFromStable>();
steps.Add(typeof(ScreenBehaviour)); AddStep<ScreenBehaviour>();
Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle; Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription; Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription;
MainAreaContent.AddRange(new Drawable[]
{
content = new PopoverContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 20 },
Child = new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(minSize: 640, maxSize: 800),
new Dimension(),
},
Content = new[]
{
new[]
{
Empty(),
new InputBlockingContainer
{
Masking = true,
CornerRadius = 14,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6,
},
loading = new LoadingSpinner(),
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 20 },
Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, },
},
},
},
Empty(),
},
}
}
},
});
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -145,55 +57,6 @@ namespace osu.Game.Overlays
if (showFirstRunSetup.Value) Show(); if (showFirstRunSetup.Value) Show();
} }
[Resolved]
private ScreenFooter footer { get; set; } = null!;
public new FirstRunSetupFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FirstRunSetupFooterContent;
public override VisibilityContainer CreateFooterContent()
{
var footerContent = new FirstRunSetupFooterContent
{
ShowNextStep = showNextStep,
};
footerContent.OnLoadComplete += _ => updateButtons();
return footerContent;
}
public override bool OnBackButton()
{
if (currentStepIndex == 0)
return false;
Debug.Assert(stack != null);
stack.CurrentScreen.Exit();
currentStepIndex--;
updateButtons();
return true;
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (!e.Repeat)
{
switch (e.Action)
{
case GlobalAction.Select:
DisplayedFooterContent?.NextButton.TriggerClick();
return true;
case GlobalAction.Back:
footer.BackButton.TriggerClick();
return false;
}
}
return base.OnPressed(e);
}
public override void Show() public override void Show()
{ {
// if we are valid for display, only do so after reaching the main menu. // if we are valid for display, only do so after reaching the main menu.
@ -207,24 +70,11 @@ namespace osu.Game.Overlays
}, new[] { typeof(MainMenu) }); }, new[] { typeof(MainMenu) });
} }
protected override void PopIn()
{
base.PopIn();
content.ScaleTo(0.99f)
.ScaleTo(1, 400, Easing.OutQuint);
if (currentStepIndex == null)
showFirstStep();
}
protected override void PopOut() protected override void PopOut()
{ {
base.PopOut(); base.PopOut();
content.ScaleTo(0.99f, 400, Easing.OutQuint); if (CurrentStepIndex != null)
if (currentStepIndex != null)
{ {
notificationOverlay.Post(new SimpleNotification notificationOverlay.Post(new SimpleNotification
{ {
@ -237,112 +87,14 @@ namespace osu.Game.Overlays
}, },
}); });
} }
else
{
stack?.FadeOut(100)
.Expire();
}
} }
private void showFirstStep() protected override void ShowNextStep()
{ {
Debug.Assert(currentStepIndex == null); base.ShowNextStep();
screenContent.Child = stack = new ScreenStack if (CurrentStepIndex == null)
{
RelativeSizeAxes = Axes.Both,
};
currentStepIndex = -1;
showNextStep();
}
private void showNextStep()
{
Debug.Assert(currentStepIndex != null);
Debug.Assert(stack != null);
currentStepIndex++;
if (currentStepIndex < steps.Count)
{
var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value])!;
loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200);
nextScreen.OnLoadComplete += _ =>
{
loadingShowDelegate?.Cancel();
loading.Hide();
};
stack.Push(nextScreen);
}
else
{
showFirstRunSetup.Value = false; showFirstRunSetup.Value = false;
currentStepIndex = null;
Hide();
}
updateButtons();
}
private void updateButtons() => DisplayedFooterContent?.UpdateButtons(currentStepIndex, steps);
public partial class FirstRunSetupFooterContent : VisibilityContainer
{
public ShearedButton NextButton { get; private set; } = null!;
public Action? ShowNextStep;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
InternalChild = NextButton = new ShearedButton(0)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Right = 12f },
RelativeSizeAxes = Axes.X,
Width = 1,
Text = FirstRunSetupOverlayStrings.GetStarted,
DarkerColour = colourProvider.Colour2,
LighterColour = colourProvider.Colour1,
Action = () => ShowNextStep?.Invoke(),
};
}
public void UpdateButtons(int? currentStep, IReadOnlyList<Type> steps)
{
NextButton.Enabled.Value = currentStep != null;
if (currentStep == null)
return;
bool isFirstStep = currentStep == 0;
bool isLastStep = currentStep == steps.Count - 1;
if (isFirstStep)
NextButton.Text = FirstRunSetupOverlayStrings.GetStarted;
else
{
NextButton.Text = isLastStep
? CommonStrings.Finish
: LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})");
}
}
protected override void PopIn()
{
this.FadeIn();
}
protected override void PopOut()
{
this.Delay(400).FadeOut();
}
} }
} }
} }

View File

@ -0,0 +1,288 @@
// 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.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.Footer;
namespace osu.Game.Overlays
{
public partial class WizardOverlay : ShearedOverlayContainer
{
private ScreenStack? stack;
public ShearedButton? NextButton => DisplayedFooterContent?.NextButton;
protected int? CurrentStepIndex { get; private set; }
/// <summary>
/// The currently displayed screen, if any.
/// </summary>
public WizardScreen? CurrentScreen => (WizardScreen?)stack?.CurrentScreen;
private readonly List<Type> steps = new List<Type>();
private Container screenContent = null!;
private Container content = null!;
private LoadingSpinner loading = null!;
private ScheduledDelegate? loadingShowDelegate;
protected WizardOverlay(OverlayColourScheme scheme)
: base(scheme)
{
}
[BackgroundDependencyLoader]
private void load()
{
MainAreaContent.AddRange(new Drawable[]
{
content = new PopoverContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 20 },
Child = new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(minSize: 640, maxSize: 800),
new Dimension(),
},
Content = new[]
{
new[]
{
Empty(),
new InputBlockingContainer
{
Masking = true,
CornerRadius = 14,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6,
},
loading = new LoadingSpinner(),
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 20 },
Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, },
},
},
},
Empty(),
},
}
}
},
});
}
[Resolved]
private ScreenFooter footer { get; set; } = null!;
public new WizardFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as WizardFooterContent;
public override VisibilityContainer CreateFooterContent()
{
var footerContent = new WizardFooterContent
{
ShowNextStep = ShowNextStep,
};
footerContent.OnLoadComplete += _ => updateButtons();
return footerContent;
}
public override bool OnBackButton()
{
if (CurrentStepIndex == 0)
return false;
Debug.Assert(stack != null);
stack.CurrentScreen.Exit();
CurrentStepIndex--;
updateButtons();
return true;
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (!e.Repeat)
{
switch (e.Action)
{
case GlobalAction.Select:
DisplayedFooterContent?.NextButton.TriggerClick();
return true;
case GlobalAction.Back:
footer.BackButton.TriggerClick();
return false;
}
}
return base.OnPressed(e);
}
protected override void PopIn()
{
base.PopIn();
content.ScaleTo(0.99f)
.ScaleTo(1, 400, Easing.OutQuint);
if (CurrentStepIndex == null)
showFirstStep();
}
protected override void PopOut()
{
base.PopOut();
content.ScaleTo(0.99f, 400, Easing.OutQuint);
if (CurrentStepIndex == null)
{
stack?.FadeOut(100)
.Expire();
}
}
protected void AddStep<T>()
where T : WizardScreen
{
steps.Add(typeof(T));
}
private void showFirstStep()
{
Debug.Assert(CurrentStepIndex == null);
screenContent.Child = stack = new ScreenStack
{
RelativeSizeAxes = Axes.Both,
};
CurrentStepIndex = -1;
ShowNextStep();
}
protected virtual void ShowNextStep()
{
Debug.Assert(CurrentStepIndex != null);
Debug.Assert(stack != null);
CurrentStepIndex++;
if (CurrentStepIndex < steps.Count)
{
var nextScreen = (Screen)Activator.CreateInstance(steps[CurrentStepIndex.Value])!;
loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200);
nextScreen.OnLoadComplete += _ =>
{
loadingShowDelegate?.Cancel();
loading.Hide();
};
stack.Push(nextScreen);
}
else
{
CurrentStepIndex = null;
Hide();
}
updateButtons();
}
private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, steps);
public partial class WizardFooterContent : VisibilityContainer
{
public ShearedButton NextButton { get; private set; } = null!;
public Action? ShowNextStep;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
InternalChild = NextButton = new ShearedButton(0)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Right = 12f },
RelativeSizeAxes = Axes.X,
Width = 1,
Text = FirstRunSetupOverlayStrings.GetStarted,
DarkerColour = colourProvider.Colour2,
LighterColour = colourProvider.Colour1,
Action = () => ShowNextStep?.Invoke(),
};
}
public void UpdateButtons(int? currentStep, IReadOnlyList<Type> steps)
{
NextButton.Enabled.Value = currentStep != null;
if (currentStep == null)
return;
bool isFirstStep = currentStep == 0;
bool isLastStep = currentStep == steps.Count - 1;
if (isFirstStep)
NextButton.Text = FirstRunSetupOverlayStrings.GetStarted;
else
{
NextButton.Text = isLastStep
? CommonStrings.Finish
: LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})");
}
}
protected override void PopIn()
{
this.FadeIn();
}
protected override void PopOut()
{
this.Delay(400).FadeOut();
}
}
}
}

View File

@ -13,9 +13,9 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays
{ {
public abstract partial class FirstRunSetupScreen : Screen public abstract partial class WizardScreen : Screen
{ {
private const float offset = 100; private const float offset = 100;

View File

@ -577,6 +577,11 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public virtual bool AllowGameplayOverlays => true; public virtual bool AllowGameplayOverlays => true;
/// <summary>
/// On mobile devices, this specifies whether this ruleset requires the device to be in portrait orientation.
/// </summary>
public virtual bool RequiresPortraitOrientation => false;
/// <summary> /// <summary>
/// Sets a replay to be used, overriding local input. /// Sets a replay to be used, overriding local input.
/// </summary> /// </summary>

View File

@ -61,6 +61,17 @@ namespace osu.Game.Screens
/// </summary> /// </summary>
bool HideMenuCursorOnNonMouseInput { get; } bool HideMenuCursorOnNonMouseInput { get; }
/// <summary>
/// On mobile phones, this specifies whether this <see cref="OsuScreen"/> requires the device to be in portrait orientation.
/// Tablet devices are unaffected by this property.
/// </summary>
/// <remarks>
/// By default, all screens in the game display in landscape orientation on phones.
/// Setting this to <c>true</c> will display this screen in portrait orientation instead,
/// and switch back to landscape when transitioning back to a regular non-portrait screen.
/// </remarks>
bool RequiresPortraitOrientation { get; }
/// <summary> /// <summary>
/// Whether overlays should be able to be opened when this screen is current. /// Whether overlays should be able to be opened when this screen is current.
/// </summary> /// </summary>

View File

@ -47,6 +47,8 @@ namespace osu.Game.Screens
public virtual bool HideMenuCursorOnNonMouseInput => false; public virtual bool HideMenuCursorOnNonMouseInput => false;
public virtual bool RequiresPortraitOrientation => false;
/// <summary> /// <summary>
/// The initial overlay activation mode to use when this screen is entered for the first time. /// The initial overlay activation mode to use when this screen is entered for the first time.
/// </summary> /// </summary>

View File

@ -68,6 +68,17 @@ namespace osu.Game.Screens.Play
public override bool HideMenuCursorOnNonMouseInput => true; public override bool HideMenuCursorOnNonMouseInput => true;
public override bool RequiresPortraitOrientation
{
get
{
if (!LoadedBeatmapSuccessfully)
return false;
return DrawableRuleset!.RequiresPortraitOrientation;
}
}
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
// We are managing our own adjustments (see OnEntering/OnExiting). // We are managing our own adjustments (see OnEntering/OnExiting).

View File

@ -53,6 +53,9 @@ namespace osu.Game.Screens.Play
public override bool? AllowGlobalTrackControl => false; public override bool? AllowGlobalTrackControl => false;
// this makes the game stay in portrait mode when restarting gameplay rather than switching back to landscape.
public override bool RequiresPortraitOrientation => CurrentPlayer?.RequiresPortraitOrientation == true;
public override float BackgroundParallaxAmount => quickRestart ? 0 : 1; public override float BackgroundParallaxAmount => quickRestart ? 0 : 1;
// Here because IsHovered will not update unless we do so. // Here because IsHovered will not update unless we do so.

View File

@ -152,7 +152,7 @@ namespace osu.Game.Screens.Play
Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important); Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important);
break; break;
case @"invalid beatmap_hash": case @"invalid or missing beatmap_hash":
Logger.Log($"This beatmap does not match the online version. Please update or redownload it.\n\n{whatWillHappen}", level: LogLevel.Important); Logger.Log($"This beatmap does not match the online version. Please update or redownload it.\n\n{whatWillHappen}", level: LogLevel.Important);
break; break;

View File

@ -2,7 +2,6 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -91,6 +90,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
private readonly ScoreInfo score; private readonly ScoreInfo score;
[Resolved]
private ResultsScreen? resultsScreen { get; set; }
private CircularProgress accuracyCircle = null!; private CircularProgress accuracyCircle = null!;
private GradedCircles gradedCircles = null!; private GradedCircles gradedCircles = null!;
private Container<RankBadge> badges = null!; private Container<RankBadge> badges = null!;
@ -101,7 +103,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
private PoolableSkinnableSample? badgeMaxSound; private PoolableSkinnableSample? badgeMaxSound;
private PoolableSkinnableSample? swooshUpSound; private PoolableSkinnableSample? swooshUpSound;
private PoolableSkinnableSample? rankImpactSound; private PoolableSkinnableSample? rankImpactSound;
private PoolableSkinnableSample? rankApplauseSound;
private readonly Bindable<double> tickPlaybackRate = new Bindable<double>(); private readonly Bindable<double> tickPlaybackRate = new Bindable<double>();
@ -197,15 +198,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
if (withFlair) if (withFlair)
{ {
var applauseSamples = new List<string> { applauseSampleName };
if (score.Rank >= ScoreRank.B)
// when rank is B or higher, play legacy applause sample on legacy skins.
applauseSamples.Insert(0, @"applause");
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)), rankImpactSound = new PoolableSkinnableSample(new SampleInfo(impactSampleName)),
rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())),
scoreTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/score-tick")), scoreTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/score-tick")),
badgeTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink")), badgeTickSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink")),
badgeMaxSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink-max")), badgeMaxSound = new PoolableSkinnableSample(new SampleInfo(@"Results/badge-dink-max")),
@ -333,16 +328,9 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
}); });
const double applause_pre_delay = 545f; const double applause_pre_delay = 545f;
const double applause_volume = 0.8f;
using (BeginDelayedSequence(applause_pre_delay)) using (BeginDelayedSequence(applause_pre_delay))
{ Schedule(() => resultsScreen?.PlayApplause(score.Rank));
Schedule(() =>
{
rankApplauseSound!.VolumeTo(applause_volume);
rankApplauseSound!.Play();
});
}
} }
} }
@ -384,34 +372,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
} }
} }
private string applauseSampleName
{
get
{
switch (score.Rank)
{
default:
case ScoreRank.D:
return @"Results/applause-d";
case ScoreRank.C:
return @"Results/applause-c";
case ScoreRank.B:
return @"Results/applause-b";
case ScoreRank.A:
return @"Results/applause-a";
case ScoreRank.S:
case ScoreRank.SH:
case ScoreRank.X:
case ScoreRank.XH:
return @"Results/applause-s";
}
}
}
private string impactSampleName private string impactSampleName
{ {
get get

View File

@ -17,6 +17,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -29,10 +30,12 @@ using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking.Expanded.Accuracy; using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Screens.Ranking namespace osu.Game.Screens.Ranking
{ {
[Cached]
public abstract partial class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction> public abstract partial class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>
{ {
protected const float BACKGROUND_BLUR = 20; protected const float BACKGROUND_BLUR = 20;
@ -263,6 +266,64 @@ namespace osu.Game.Screens.Ranking
} }
} }
#region Applause
private PoolableSkinnableSample? rankApplauseSound;
public void PlayApplause(ScoreRank rank)
{
const double applause_volume = 0.8f;
if (!this.IsCurrentScreen())
return;
rankApplauseSound?.Dispose();
var applauseSamples = new List<string>();
if (rank >= ScoreRank.B)
// when rank is B or higher, play legacy applause sample on legacy skins.
applauseSamples.Insert(0, @"applause");
switch (rank)
{
default:
case ScoreRank.D:
applauseSamples.Add(@"Results/applause-d");
break;
case ScoreRank.C:
applauseSamples.Add(@"Results/applause-c");
break;
case ScoreRank.B:
applauseSamples.Add(@"Results/applause-b");
break;
case ScoreRank.A:
applauseSamples.Add(@"Results/applause-a");
break;
case ScoreRank.S:
case ScoreRank.SH:
case ScoreRank.X:
case ScoreRank.XH:
applauseSamples.Add(@"Results/applause-s");
break;
}
LoadComponentAsync(rankApplauseSound = new PoolableSkinnableSample(new SampleInfo(applauseSamples.ToArray())), s =>
{
if (!this.IsCurrentScreen() || s != rankApplauseSound)
return;
rankApplauseSound.VolumeTo(applause_volume);
rankApplauseSound.Play();
});
}
#endregion
/// <summary> /// <summary>
/// Performs a fetch/refresh of scores to be displayed. /// Performs a fetch/refresh of scores to be displayed.
/// </summary> /// </summary>
@ -330,6 +391,8 @@ namespace osu.Game.Screens.Ranking
if (!skipExitTransition) if (!skipExitTransition)
this.FadeOut(100); this.FadeOut(100);
rankApplauseSound?.Stop();
return false; return false;
} }

View File

@ -0,0 +1,49 @@
// 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.Game.Screens;
using osu.Game.Screens.Play;
namespace osu.Game.Utils
{
public static class MobileUtils
{
/// <summary>
/// Determines the correct <see cref="Orientation"/> state which a mobile device should be put into for the given information.
/// </summary>
/// <param name="userPlayInfo">Information about whether the user is currently playing.</param>
/// <param name="currentScreen">The current screen which the user is at.</param>
/// <param name="isTablet">Whether the user is playing on a mobile tablet device instead of a phone.</param>
public static Orientation GetOrientation(ILocalUserPlayInfo userPlayInfo, IOsuScreen currentScreen, bool isTablet)
{
bool lockCurrentOrientation = userPlayInfo.PlayingState.Value == LocalUserPlayingState.Playing;
bool lockToPortraitOnPhone = currentScreen.RequiresPortraitOrientation;
if (lockToPortraitOnPhone && !isTablet)
return Orientation.Portrait;
if (lockCurrentOrientation)
return Orientation.Locked;
return Orientation.Default;
}
public enum Orientation
{
/// <summary>
/// Lock the game orientation.
/// </summary>
Locked,
/// <summary>
/// Lock the game to portrait orientation (does not include upside-down portrait).
/// </summary>
Portrait,
/// <summary>
/// Use the application's default settings.
/// </summary>
Default,
}
}
}

View File

@ -1,14 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using Foundation; using Foundation;
using osu.Framework.iOS; using osu.Framework.iOS;
using UIKit;
namespace osu.iOS namespace osu.iOS
{ {
[Register("AppDelegate")] [Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate public class AppDelegate : GameApplicationDelegate
{ {
protected override Framework.Game CreateGame() => new OsuGameIOS(); private UIInterfaceOrientationMask? defaultOrientationsMask;
private UIInterfaceOrientationMask? orientations;
/// <summary>
/// The current orientation the game is displayed in.
/// </summary>
public UIInterfaceOrientation CurrentOrientation => Host.Window.UIWindow.WindowScene!.InterfaceOrientation;
/// <summary>
/// Controls the orientations allowed for the device to rotate to, overriding the default allowed orientations.
/// </summary>
public UIInterfaceOrientationMask? Orientations
{
get => orientations;
set
{
if (orientations == value)
return;
orientations = value;
if (OperatingSystem.IsIOSVersionAtLeast(16))
Host.Window.ViewController.SetNeedsUpdateOfSupportedInterfaceOrientations();
else
UIViewController.AttemptRotationToDeviceOrientation();
}
}
protected override Framework.Game CreateGame() => new OsuGameIOS(this);
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow)
{
if (orientations != null)
return orientations.Value;
if (defaultOrientationsMask == null)
{
defaultOrientationsMask = 0;
var defaultOrientations = (NSArray)NSBundle.MainBundle.ObjectForInfoDictionary("UISupportedInterfaceOrientations");
foreach (var value in defaultOrientations.ToArray<NSString>())
defaultOrientationsMask |= Enum.Parse<UIInterfaceOrientationMask>(value.ToString().Replace("UIInterfaceOrientation", string.Empty));
}
return defaultOrientationsMask.Value;
}
} }
} }

View File

@ -8,17 +8,60 @@ using osu.Framework.Graphics;
using osu.Framework.iOS; using osu.Framework.iOS;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.Screens;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
using UIKit;
namespace osu.iOS namespace osu.iOS
{ {
public partial class OsuGameIOS : OsuGame public partial class OsuGameIOS : OsuGame
{ {
private readonly AppDelegate appDelegate;
public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString());
public override bool HideUnlicensedContent => true; public override bool HideUnlicensedContent => true;
public OsuGameIOS(AppDelegate appDelegate)
{
this.appDelegate = appDelegate;
}
protected override void LoadComplete()
{
base.LoadComplete();
UserPlayingState.BindValueChanged(_ => updateOrientation());
}
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
{
base.ScreenChanged(current, newScreen);
if (newScreen != null)
updateOrientation();
}
private void updateOrientation()
{
bool iPad = UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad;
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, iPad);
switch (orientation)
{
case MobileUtils.Orientation.Locked:
appDelegate.Orientations = (UIInterfaceOrientationMask)(1 << (int)appDelegate.CurrentOrientation);
break;
case MobileUtils.Orientation.Portrait:
appDelegate.Orientations = UIInterfaceOrientationMask.Portrait;
break;
case MobileUtils.Orientation.Default:
appDelegate.Orientations = null;
break;
}
}
protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier(); protected override UpdateManager CreateUpdateManager() => new MobileUpdateNotifier();
protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo(); protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();