diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d8645d728e..d75f09f184 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -114,10 +114,7 @@ jobs:
dotnet-version: "8.0.x"
- name: Install .NET workloads
- # since windows image 20241113.3.0, not specifying a version here
- # installs the .NET 7 version of android workload for very unknown reasons.
- # revisit once we upgrade to .NET 9, it's probably fixed there.
- run: dotnet workload install android --version (dotnet --version)
+ run: dotnet workload install android
- name: Compile
run: dotnet build -c Debug osu.Android.slnf
diff --git a/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000000..4432459b86
--- /dev/null
+++ b/.idea/.idea.osu.Android/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 550f7c8e11..08b79fc2c0 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -18,3 +18,6 @@ M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize(
M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead.
+M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead.
+M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
+M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 1d368e9bd1..86f73a37d4 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -9,7 +9,7 @@
false
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index d69bc78b8f..51c0233942 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,7 +9,7 @@
false
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index 7ac269f65f..ed4e8631ea 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -9,7 +9,7 @@
false
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index d69bc78b8f..51c0233942 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -9,7 +9,7 @@
false
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
index 0a4fa84ce1..dd8337abee 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon/Beatmaps/PippidonBeatmapConverter.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . 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.Linq;
using System.Threading;
@@ -9,7 +10,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.UI;
-using osuTK;
namespace osu.Game.Rulesets.Pippidon.Beatmaps
{
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
};
}
- private int getLane(HitObject hitObject) => (int)MathHelper.Clamp(
+ private int getLane(HitObject hitObject) => (int)Math.Clamp(
(getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1);
private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X;
diff --git a/osu.Android.props b/osu.Android.props
index 7ae16b8b70..b6ab7dc712 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -10,7 +10,7 @@
true
-
+
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 7b0a027d39..486979487b 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -17,6 +17,6 @@
-all
-
+
diff --git a/osu.iOS/AppDelegate.cs b/osu.iOS/AppDelegate.cs
index e88b39f710..5d309f2fc1 100644
--- a/osu.iOS/AppDelegate.cs
+++ b/osu.iOS/AppDelegate.cs
@@ -1,14 +1,61 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using Foundation;
using osu.Framework.iOS;
+using UIKit;
namespace osu.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameApplicationDelegate
{
- protected override Framework.Game CreateGame() => new OsuGameIOS();
+ private UIInterfaceOrientationMask? defaultOrientationsMask;
+ private UIInterfaceOrientationMask? orientations;
+
+ ///
+ /// The current orientation the game is displayed in.
+ ///
+ public UIInterfaceOrientation CurrentOrientation => Host.Window.UIWindow.WindowScene!.InterfaceOrientation;
+
+ ///
+ /// Controls the orientations allowed for the device to rotate to, overriding the default allowed orientations.
+ ///
+ 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())
+ defaultOrientationsMask |= Enum.Parse(value.ToString().Replace("UIInterfaceOrientation", string.Empty));
+ }
+
+ return defaultOrientationsMask.Value;
+ }
}
}
diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist
index 70747fc9c8..120e8caecc 100644
--- a/osu.iOS/Info.plist
+++ b/osu.iOS/Info.plist
@@ -153,6 +153,8 @@
Editor
+ ITSAppUsesNonExemptEncryption
+
LSApplicationCategoryType
public.app-category.music-games
LSSupportsOpeningDocumentsInPlace
diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs
index a9ca1778a0..96b8fb9804 100644
--- a/osu.iOS/OsuGameIOS.cs
+++ b/osu.iOS/OsuGameIOS.cs
@@ -8,17 +8,63 @@ using osu.Framework.Graphics;
using osu.Framework.iOS;
using osu.Framework.Platform;
using osu.Game;
+using osu.Game.Screens;
using osu.Game.Updater;
using osu.Game.Utils;
+using osuTK;
+using UIKit;
namespace osu.iOS
{
public partial class OsuGameIOS : OsuGame
{
+ private readonly AppDelegate appDelegate;
public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString());
public override bool HideUnlicensedContent => true;
+ public override Vector2 ScalingContainerTargetDrawSize => new Vector2(1024, 1024 * DrawHeight / DrawWidth);
+
+ 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() => UIApplication.SharedApplication.InvokeOnMainThread(() =>
+ {
+ 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 BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();