1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-24 19:02:59 +08:00

Merge branch 'master' into collection-counts

This commit is contained in:
Dean Herbert 2023-04-03 15:25:56 +09:00
commit 01e489e9c3
49 changed files with 282 additions and 312 deletions

View File

@ -13,17 +13,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
@ -77,10 +77,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
@ -94,7 +94,7 @@ jobs:
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
- name: Upload Test Results
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: ${{ always() }}
with:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
@ -106,10 +106,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"
@ -125,10 +125,10 @@ jobs:
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "6.0.x"

View File

@ -48,8 +48,8 @@ jobs:
CONTINUE="no"
fi
echo "::set-output name=continue::${CONTINUE}"
echo "::set-output name=matrix::${MATRIX_JSON}"
echo "continue=${CONTINUE}" >> $GITHUB_OUTPUT
echo "matrix=${MATRIX_JSON}" >> $GITHUB_OUTPUT
diffcalc:
name: Run
runs-on: self-hosted
@ -80,34 +80,34 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "::set-output name=branchname::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')"
echo "::set-output name=repo::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')"
echo "branchname=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')" >> $GITHUB_OUTPUT
echo "repo=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')" >> $GITHUB_OUTPUT
# Checkout osu
- name: Checkout osu (master)
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: 'master/osu'
- name: Checkout osu (pr)
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: 'pr/osu'
repository: ${{ steps.upstreambranch.outputs.repo }}
ref: ${{ steps.upstreambranch.outputs.branchname }}
- name: Checkout osu-difficulty-calculator (master)
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: ppy/osu-difficulty-calculator
path: 'master/osu-difficulty-calculator'
- name: Checkout osu-difficulty-calculator (pr)
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: ppy/osu-difficulty-calculator
path: 'pr/osu-difficulty-calculator'
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v3
with:
dotnet-version: "5.0.x"

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0

View File

@ -105,7 +105,7 @@ When it comes to contributing to the project, the two main things you can do to
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
For those interested, we love to reward quality contributions via [bounties](https://docs.google.com/spreadsheets/d/1jNXfj_S3Pb5PErA-czDdC9DUu4IgUbe1Lt8E7CYUJuE/view?&rm=minimal#gid=523803337), paid out via PayPal or osu!supporter tags. Don't hesitate to [request a bounty](https://docs.google.com/forms/d/e/1FAIpQLSet_8iFAgPMG526pBZ2Kic6HSh7XPM3fE8xPcnWNkMzINDdYg/viewform) for your work on this project.
We love to reward quality contributions. If you have made a large contribution, or are a regular contributor, you are welcome to [submit an expense via opencollective](https://opencollective.com/ppy/expenses/new). If you have any questions, feel free to [reach out to peppy](mailto:pe@ppy.sh) before doing so.
## Licence

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.314.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.403.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -1,17 +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.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,9 +1,8 @@
// 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.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Catch.Tests.iOS
{
@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Catch.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}

View File

@ -136,6 +136,7 @@ namespace osu.Game.Rulesets.Catch.UI
Origin = Anchor.TopCentre;
Size = new Vector2(BASE_SIZE);
if (difficulty != null)
Scale = calculateScale(difficulty);
@ -333,8 +334,11 @@ namespace osu.Game.Rulesets.Catch.UI
base.Update();
var scaleFromDirection = new Vector2((int)VisualDirection, 1);
body.Scale = scaleFromDirection;
caughtObjectContainer.Scale = hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Inverse of catcher scale is applied here, as catcher gets scaled by circle size and so do the incoming fruit.
caughtObjectContainer.Scale = (1 / Scale.X) * (flipCatcherPlate ? scaleFromDirection : Vector2.One);
hitExplosionContainer.Scale = flipCatcherPlate ? scaleFromDirection : Vector2.One;
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||

View File

@ -1,17 +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.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,9 +1,8 @@
// 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.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Mania.Tests.iOS
{
@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}

View File

@ -3,6 +3,9 @@
#nullable disable
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
@ -10,5 +13,19 @@ namespace osu.Game.Rulesets.Mania.Tests
public partial class TestSceneManiaPlayer : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("change direction to down", () => changeDirectionTo(ManiaScrollingDirection.Down));
AddStep("change direction to up", () => changeDirectionTo(ManiaScrollingDirection.Up));
}
private void changeDirectionTo(ManiaScrollingDirection direction)
{
var rulesetConfig = (ManiaRulesetConfigManager)RulesetConfigs.GetConfigFor(new ManiaRuleset()).AsNonNull();
rulesetConfig.SetValue(ManiaRulesetSetting.ScrollDirection, direction);
}
}
}

View File

@ -43,8 +43,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
largeFaint = new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Height = ArgonNotePiece.NOTE_ACCENT_RATIO,
Masking = true,
@ -81,11 +79,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
if (direction.NewValue == ScrollingDirection.Up)
{
Anchor = Anchor.TopCentre;
largeFaint.Anchor = Anchor.TopCentre;
largeFaint.Origin = Anchor.TopCentre;
Y = ArgonNotePiece.NOTE_HEIGHT / 2;
}
else
{
Anchor = Anchor.BottomCentre;
largeFaint.Anchor = Anchor.BottomCentre;
largeFaint.Origin = Anchor.BottomCentre;
Y = -ArgonNotePiece.NOTE_HEIGHT / 2;
}
}

View File

@ -1,17 +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.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,9 +1,8 @@
// 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.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Osu.Tests.iOS
{
@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
protected override bool AlwaysShowWhenSelected => true;
protected override bool ShouldBeAlive => base.ShouldBeAlive
|| (ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
|| (DrawableObject is not DrawableSpinner && ShowHitMarkers.Value && editorClock.CurrentTime >= Item.StartTime && editorClock.CurrentTime - Item.GetEndTime() < HitCircleOverlapMarker.FADE_OUT_EXTENSION);
protected OsuSelectionBlueprint(T hitObject)
: base(hitObject)

View File

@ -252,13 +252,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
renderer.SetBlend(BlendingParameters.Additive);
renderer.PushLocalMatrix(DrawInfo.Matrix);
TextureShader.Bind();
BindTextureShader(renderer);
texture.Bind();
for (int i = 0; i < points.Count; i++)
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
TextureShader.Unbind();
UnbindTextureShader(renderer);
renderer.PopLocalMatrix();
}

View File

@ -1,17 +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.
#nullable disable
using Foundation;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,9 +1,8 @@
// 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.
#nullable disable
using UIKit;
using osu.Framework.iOS;
using osu.Game.Tests;
namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
@ -11,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}

View File

@ -1,16 +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.
#nullable disable
using Foundation;
using osu.Framework.iOS;
namespace osu.Game.Tests.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
protected override Framework.Game CreateGame() => new OsuTestBrowser();
}
}

View File

@ -1,9 +1,7 @@
// 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.
#nullable disable
using UIKit;
using osu.Framework.iOS;
namespace osu.Game.Tests.iOS
{
@ -11,7 +9,7 @@ namespace osu.Game.Tests.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuTestBrowser());
}
}
}

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using NUnit.Framework;
@ -51,9 +49,11 @@ namespace osu.Game.Tests.Testing
[Test]
public void TestRetrieveShader()
{
AddAssert("ruleset shaders retrieved", () =>
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestVertex.vs") != null &&
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestFragment.fs") != null);
AddStep("ruleset shaders retrieved without error", () =>
{
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestVertex.vs");
Dependencies.Get<ShaderManager>().LoadRaw(@"sh_TestFragment.fs");
});
}
[Test]
@ -76,12 +76,12 @@ namespace osu.Game.Tests.Testing
}
public override IResourceStore<byte[]> CreateResourceStore() => new NamespacedResourceStore<byte[]>(TestResources.GetStore(), @"Resources");
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new TestRulesetConfigManager();
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TestRulesetConfigManager();
public override IEnumerable<Mod> GetModsFor(ModType type) => Array.Empty<Mod>();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => null;
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => null!;
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null!;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null!;
}
private class TestRulesetConfigManager : IRulesetConfigManager

View File

@ -100,8 +100,10 @@ namespace osu.Game.Tests.Visual.Background
private IUniformBuffer<TriangleBorderData>? borderDataBuffer;
public override void Draw(IRenderer renderer)
protected override void BindUniformResources(IShader shader, IRenderer renderer)
{
base.BindUniformResources(shader, renderer);
borderDataBuffer ??= renderer.CreateUniformBuffer<TriangleBorderData>();
borderDataBuffer.Data = borderDataBuffer.Data with
{
@ -109,9 +111,7 @@ namespace osu.Game.Tests.Visual.Background
TexelSize = texelSize
};
TextureShader.BindUniformBlock("m_BorderData", borderDataBuffer);
base.Draw(renderer);
shader.BindUniformBlock("m_BorderData", borderDataBuffer);
}
protected override bool CanDrawOpaqueInterior => false;

View File

@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
@ -106,6 +107,26 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
}
[Test]
public void TestSaveFailedReplayWithStoryboardEndedDoesNotProgress()
{
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, _) => true);
AddStep("set storyboard duration to 0s", () => currentStoryboardDuration = 0);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
AddUntilStep("wait for button clickable", () => Player.ChildrenOfType<SaveFailedScoreButton>().First().ChildrenOfType<OsuClickableContainer>().First().Enabled.Value);
AddStep("click save button", () => Player.ChildrenOfType<SaveFailedScoreButton>().First().ChildrenOfType<OsuClickableContainer>().First().TriggerClick());
// Test a regression where importing the fail replay would cause progression to results screen in a failed state.
AddWaitStep("wait some", 10);
AddAssert("player is still current screen", () => Player.IsCurrentScreen());
}
[Test]
public void TestShowResultsFalse()
{

View File

@ -126,6 +126,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 13926,
TournamentId = 35,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg",
Image = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US@2x.jpg",
},
Badges = new[]
{

View File

@ -332,13 +332,6 @@ namespace osu.Game.Tournament
private void saveChanges()
{
foreach (var r in ladder.Rounds)
r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList();
ladder.Progressions = ladder.Matches.Where(p => p.Progression.Value != null).Select(p => new TournamentProgression(p.ID, p.Progression.Value.ID)).Concat(
ladder.Matches.Where(p => p.LosersProgression.Value != null).Select(p => new TournamentProgression(p.ID, p.LosersProgression.Value.ID, true)))
.ToList();
// Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state.
string serialisedLadder = GetSerialisedLadder();
@ -349,6 +342,13 @@ namespace osu.Game.Tournament
public string GetSerialisedLadder()
{
foreach (var r in ladder.Rounds)
r.Matches = ladder.Matches.Where(p => p.Round.Value == r).Select(p => p.ID).ToList();
ladder.Progressions = ladder.Matches.Where(p => p.Progression.Value != null).Select(p => new TournamentProgression(p.ID, p.Progression.Value.ID)).Concat(
ladder.Matches.Where(p => p.LosersProgression.Value != null).Select(p => new TournamentProgression(p.ID, p.LosersProgression.Value.ID, true)))
.ToList();
return JsonConvert.SerializeObject(ladder,
new JsonSerializerSettings
{

View File

@ -306,7 +306,7 @@ namespace osu.Game.Graphics.Backgrounds
};
shader.Bind();
shader.BindUniformBlock("m_BorderData", borderDataBuffer);
shader.BindUniformBlock(@"m_BorderData", borderDataBuffer);
foreach (TriangleParticle particle in parts)
{

View File

@ -249,7 +249,7 @@ namespace osu.Game.Graphics.Backgrounds
};
shader.Bind();
shader.BindUniformBlock("m_BorderData", borderDataBuffer);
shader.BindUniformBlock(@"m_BorderData", borderDataBuffer);
Vector2 relativeSize = Vector2.Divide(triangleSize, size);

View File

@ -76,17 +76,24 @@ namespace osu.Game.Graphics.Sprites
private IUniformBuffer<AnimationData> animationDataBuffer;
private IVertexBatch<LogoAnimationVertex> animationVertexBatch;
protected override void Blit(IRenderer renderer)
protected override void BindUniformResources(IShader shader, IRenderer renderer)
{
if (DrawRectangle.Width == 0 || DrawRectangle.Height == 0)
return;
base.BindUniformResources(shader, renderer);
animationDataBuffer ??= renderer.CreateUniformBuffer<AnimationData>();
animationVertexBatch ??= renderer.CreateQuadBatch<LogoAnimationVertex>(1, 2);
animationDataBuffer.Data = animationDataBuffer.Data with { Progress = progress };
TextureShader.BindUniformBlock("m_AnimationData", animationDataBuffer);
shader.BindUniformBlock(@"m_AnimationData", animationDataBuffer);
}
protected override void Blit(IRenderer renderer)
{
if (DrawRectangle.Width == 0 || DrawRectangle.Height == 0)
return;
base.Blit(renderer);
renderer.DrawQuad(
Texture,

View File

@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString RendererHeader => new TranslatableString(getKey(@"renderer_header"), @"Renderer");
/// <summary>
/// "Renderer"
/// </summary>
public static LocalisableString Renderer => new TranslatableString(getKey(@"renderer"), @"Renderer");
/// <summary>
/// "Frame limiter"
/// </summary>
@ -144,6 +149,12 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Png => new TranslatableString(getKey(@"png_lossless"), @"PNG (lossless)");
/// <summary>
/// "In order to change the renderer, the game will close. Please open it again."
/// </summary>
public static LocalisableString ChangeRendererConfirmation =>
new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again.");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -50,16 +50,18 @@ namespace osu.Game.Localisation
public static LocalisableString NoAutoplayMod => new TranslatableString(getKey(@"no_autoplay_mod"), @"The current ruleset doesn't have an autoplay mod available!");
/// <summary>
/// "osu! doesn&#39;t seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting."
/// "osu! doesn&#39;t seem to be able to play audio correctly.
///
/// Please try changing your audio device to a working setting."
/// </summary>
public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"),
@"osu! doesn't seem to be able to play audio correctly.\n\nPlease try changing your audio device to a working setting.");
public static LocalisableString AudioPlaybackIssue => new TranslatableString(getKey(@"audio_playback_issue"), @"osu! doesn't seem to be able to play audio correctly.
Please try changing your audio device to a working setting.");
/// <summary>
/// "The score overlay is currently disabled. You can toggle this by pressing {0}."
/// </summary>
public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"),
@"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0);
public static LocalisableString ScoreOverlayDisabled(LocalisableString arg0) => new TranslatableString(getKey(@"score_overlay_disabled"), @"The score overlay is currently disabled. You can toggle this by pressing {0}.", arg0);
private static string getKey(string key) => $@"{prefix}:{key}";
}

View File

@ -65,6 +65,11 @@ namespace osu.Game.Localisation
if (manager == null)
return null;
// When using the English culture, prefer the fallbacks rather than osu-resources baked strings.
// They are guaranteed to be up-to-date, and is also what a developer expects to see when making changes to `xxxStrings.cs` files.
if (EffectiveCulture.Name == @"en")
return null;
try
{
return manager.GetString(key, EffectiveCulture);

View File

@ -209,7 +209,7 @@ namespace osu.Game.Overlays.AccountCreation
if (!string.IsNullOrEmpty(errors.Message))
passwordDescription.AddErrors(new[] { errors.Message });
game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}");
game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", true);
}
}
else

View File

@ -132,11 +132,10 @@ namespace osu.Game.Overlays.Comments
},
sideNumber = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
Text = "+1",
Font = OsuFont.GetFont(size: 14),
Margin = new MarginPadding { Right = 3 },
Alpha = 0,
},
votesCounter = new OsuSpriteText
@ -189,7 +188,7 @@ namespace osu.Game.Overlays.Comments
else
sideNumber.FadeTo(IsHovered ? 1 : 0);
borderContainer.BorderThickness = IsHovered ? 3 : 0;
borderContainer.BorderThickness = IsHovered ? 2 : 0;
}
private void onHoverAction()

View File

@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
set => valueText.Text = value.ToLocalisableString("N0");
}
public CountSection(LocalisableString header)
protected CountSection(LocalisableString header)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;

View File

@ -32,7 +32,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private FillFlowContainer<SettingsSlider<float>> scalingSettings = null!;
private readonly Bindable<Display> currentDisplay = new Bindable<Display>();
private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>();
private Bindable<ScalingMode> scalingMode = null!;
private Bindable<Size> sizeFullscreen = null!;
@ -75,7 +74,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
if (window != null)
{
currentDisplay.BindTo(window.CurrentDisplayBindable);
windowModes.BindTo(window.SupportedWindowModes);
window.DisplaysChanged += onDisplaysChanged;
}
@ -87,7 +85,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown = new SettingsDropdown<WindowMode>
{
LabelText = GraphicsSettingsStrings.ScreenMode,
ItemSource = windowModes,
Items = window?.SupportedWindowModes,
CanBeShown = { Value = window?.SupportedWindowModes.Count() > 1 },
Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode),
},
displayDropdown = new DisplaySettingsDropdown
@ -181,8 +180,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
updateScreenModeWarning();
}, true);
windowModes.BindCollectionChanged((_, _) => updateDisplaySettingsVisibility());
currentDisplay.BindValueChanged(display => Schedule(() =>
{
resolutions.RemoveRange(1, resolutions.Count - 1);
@ -236,7 +233,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private void updateDisplaySettingsVisibility()
{
windowModeDropdown.CanBeShown.Value = windowModes.Count > 1;
resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen;
displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1;
safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero;

View File

@ -1,15 +1,18 @@
// 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.
#nullable disable
using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Graphics
{
@ -17,12 +20,25 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{
protected override LocalisableString Header => GraphicsSettingsStrings.RendererHeader;
private bool automaticRendererInUse;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig)
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, IDialogOverlay? dialogOverlay, OsuGame? game, GameHost host)
{
// NOTE: Compatability mode omitted
var renderer = config.GetBindable<RendererType>(FrameworkSetting.Renderer);
automaticRendererInUse = renderer.Value == RendererType.Automatic;
SettingsEnumDropdown<RendererType> rendererDropdown;
Children = new Drawable[]
{
rendererDropdown = new RendererSettingsDropdown
{
LabelText = GraphicsSettingsStrings.Renderer,
Current = renderer,
Items = host.GetPreferredRenderersForCurrentPlatform().OrderBy(t => t).Where(t => t != RendererType.Vulkan),
Keywords = new[] { @"compatibility", @"directx" },
},
// TODO: this needs to be a custom dropdown at some point
new SettingsEnumDropdown<FrameSync>
{
@ -41,6 +57,55 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
Current = osuConfig.GetBindable<bool>(OsuSetting.ShowFpsDisplay)
},
};
renderer.BindValueChanged(r =>
{
if (r.NewValue == host.ResolvedRenderer)
return;
// Need to check startup renderer for the "automatic" case, as ResolvedRenderer above will track the final resolved renderer instead.
if (r.NewValue == RendererType.Automatic && automaticRendererInUse)
return;
dialogOverlay?.Push(new ConfirmDialog(GraphicsSettingsStrings.ChangeRendererConfirmation, () => game?.AttemptExit(), () =>
{
renderer.Value = automaticRendererInUse ? RendererType.Automatic : host.ResolvedRenderer;
}));
});
// TODO: remove this once we support SDL+android.
if (RuntimeInfo.OS == RuntimeInfo.Platform.Android)
{
rendererDropdown.Items = new[] { RendererType.Automatic, RendererType.OpenGLLegacy };
rendererDropdown.SetNoticeText("New renderer support for android is coming soon!", true);
}
}
private partial class RendererSettingsDropdown : SettingsEnumDropdown<RendererType>
{
protected override OsuDropdown<RendererType> CreateDropdown() => new RendererDropdown();
protected partial class RendererDropdown : DropdownControl
{
private RendererType hostResolvedRenderer;
private bool automaticRendererInUse;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager config, GameHost host)
{
var renderer = config.GetBindable<RendererType>(FrameworkSetting.Renderer);
automaticRendererInUse = renderer.Value == RendererType.Automatic;
hostResolvedRenderer = host.ResolvedRenderer;
}
protected override LocalisableString GenerateItemText(RendererType item)
{
if (item == RendererType.Automatic && automaticRendererInUse)
return LocalisableString.Interpolate($"{base.GenerateItemText(item)} ({hostResolvedRenderer.GetDescription()})");
return base.GenerateItemText(item);
}
}
}
}
}

View File

@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Mods
};
shader.Bind();
shader.BindUniformBlock("m_FlashlightParameters", flashlightParametersBuffer);
shader.BindUniformBlock(@"m_FlashlightParameters", flashlightParametersBuffer);
renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: addAction);

View File

@ -25,21 +25,28 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// The texture store to be used for the ruleset.
/// </summary>
/// <remarks>
/// Reads textures from the "Textures" folder in ruleset resources.
/// If not available locally, lookups will fallback to the global texture store.
/// </remarks>
public TextureStore TextureStore { get; }
/// <summary>
/// The sample store to be used for the ruleset.
/// </summary>
/// <remarks>
/// This is the local sample store pointing to the ruleset sample resources,
/// the cached sample store (<see cref="FallbackSampleStore"/>) retrieves from
/// this store and falls back to the parent store if this store doesn't have the requested sample.
/// Reads samples from the "Samples" folder in ruleset resources.
/// If not available locally, lookups will fallback to the global sample store.
/// </remarks>
public ISampleStore SampleStore { get; }
/// <summary>
/// The shader manager to be used for the ruleset.
/// </summary>
/// <remarks>
/// Reads shaders from the "Shaders" folder in ruleset resources.
/// If not available locally, lookups will fallback to the global shader manager.
/// </remarks>
public ShaderManager ShaderManager { get; }
/// <summary>
@ -61,8 +68,7 @@ namespace osu.Game.Rulesets.UI
SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get<ISampleStore>()));
ShaderManager = new ShaderManager(host.Renderer, new NamespacedResourceStore<byte[]>(resources, @"Shaders"));
CacheAs(ShaderManager = new FallbackShaderManager(host.Renderer, ShaderManager, parent.Get<ShaderManager>()));
CacheAs(ShaderManager = new RulesetShaderManager(host.Renderer, new NamespacedResourceStore<byte[]>(resources, @"Shaders"), parent.Get<ShaderManager>()));
RulesetConfigManager = parent.Get<IRulesetConfigCache>().GetConfigFor(ruleset);
if (RulesetConfigManager != null)
@ -190,24 +196,27 @@ namespace osu.Game.Rulesets.UI
}
}
private class FallbackShaderManager : ShaderManager
private class RulesetShaderManager : ShaderManager
{
private readonly ShaderManager primary;
private readonly ShaderManager fallback;
private readonly ShaderManager parent;
public FallbackShaderManager(IRenderer renderer, ShaderManager primary, ShaderManager fallback)
: base(renderer, new ResourceStore<byte[]>())
public RulesetShaderManager(IRenderer renderer, NamespacedResourceStore<byte[]> rulesetResources, ShaderManager parent)
: base(renderer, rulesetResources)
{
this.primary = primary;
this.fallback = fallback;
this.parent = parent;
}
public override byte[]? LoadRaw(string name) => primary.LoadRaw(name) ?? fallback.LoadRaw(name);
protected override void Dispose(bool disposing)
public override IShader Load(string vertex, string fragment)
{
base.Dispose(disposing);
if (primary.IsNotNull()) primary.Dispose();
try
{
return base.Load(vertex, fragment);
}
catch
{
// Shader lookup is very non-standard. Rather than returning null on missing shaders, exceptions are thrown.
return parent.Load(vertex, fragment);
}
}
}
}

View File

@ -130,6 +130,8 @@ namespace osu.Game.Screens
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
}

View File

@ -73,11 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private OsuSpriteText typeLabel = null!;
private LoadingLayer loadingLayer = null!;
public void SelectBeatmap()
{
if (matchSubScreen.IsCurrentScreen())
matchSubScreen.Push(new MultiplayerMatchSongSelect(matchSubScreen.Room));
}
public void SelectBeatmap() => selectBeatmapButton.TriggerClick();
[Resolved]
private MultiplayerMatchSubScreen matchSubScreen { get; set; } = null!;
@ -97,6 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private IDisposable? applyingSettingsOperation;
private Drawable playlistContainer = null!;
private DrawableRoomPlaylist drawablePlaylist = null!;
private RoundedButton selectBeatmapButton = null!;
public MatchSettings(Room room)
{
@ -275,12 +272,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.X,
Height = DrawableRoomPlaylistItem.HEIGHT
},
new RoundedButton
selectBeatmapButton = new RoundedButton
{
RelativeSizeAxes = Axes.X,
Height = 40,
Text = "Select beatmap",
Action = SelectBeatmap
Action = () =>
{
if (matchSubScreen.IsCurrentScreen())
matchSubScreen.Push(new MultiplayerMatchSongSelect(matchSubScreen.Room));
}
}
}
}

View File

@ -358,14 +358,10 @@ namespace osu.Game.Screens.Play
ScoreProcessor.RevertResult(r);
};
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{
if (storyboardEnded.NewValue)
progressToResults(true);
};
DimmableStoryboard.HasStoryboardEnded.ValueChanged += _ => checkScoreCompleted();
// Bind the judgement processors to ourselves
ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged);
ScoreProcessor.HasCompleted.BindValueChanged(_ => checkScoreCompleted());
HealthProcessor.Failed += onFail;
// Provide judgement processors to mods after they're loaded so that they're on the gameplay clock,
@ -706,19 +702,20 @@ namespace osu.Game.Screens.Play
/// <summary>
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
private void scoreCompletionChanged(ValueChangedEvent<bool> completed)
private void checkScoreCompleted()
{
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
if (!this.IsCurrentScreen())
return;
// Special case to handle rewinding post-completion. This is the only way already queued forward progress can be cancelled.
// TODO: Investigate whether this can be moved to a RewindablePlayer subclass or similar.
// Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
// In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
// but it still doesn't feel right that this exists here.
if (!completed.NewValue)
// Handle cases of arriving at this method when not in a completed state.
// - When a storyboard completion triggered this call earlier than gameplay finishes.
// - When a replay has been rewound before a queued resultsDisplayDelegate has run.
//
// Currently, even if this scenario is hit, prepareAndImportScoreAsync has already been queued (and potentially run).
// In the scenarios above, this is a non-issue, but it still feels a bit convoluted to have to cancel in this method.
// Maybe this can be improved with further refactoring.
if (!ScoreProcessor.HasCompleted.Value)
{
resultsDisplayDelegate?.Cancel();
resultsDisplayDelegate = null;
@ -742,12 +739,12 @@ namespace osu.Game.Screens.Play
if (!Configuration.ShowResults)
return;
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
bool storyboardStillRunning = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
if (storyboardHasOutro)
// If the current beatmap has a storyboard, this method will be called again on storyboard completion.
// Alternatively, the user may press the outro skip button, forcing immediate display of the results screen.
if (storyboardStillRunning)
{
// if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending
// or the user pressing the skip outro button.
skipOutroOverlay.Show();
return;
}
@ -793,6 +790,8 @@ namespace osu.Game.Screens.Play
// This player instance may already be in the process of exiting.
return;
Debug.Assert(ScoreProcessor.Rank.Value != ScoreRank.F);
this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely()));
}, Time.Current + delay, 50);

View File

@ -257,6 +257,7 @@ namespace osu.Game.Screens.Select
public FilterControlTextBox()
{
Height += filter_text_size;
TextContainer.Height *= (Height - filter_text_size) / Height;
TextContainer.Margin = new MarginPadding { Bottom = filter_text_size };
}

View File

@ -1,7 +1,6 @@
// 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.IO;
using Newtonsoft.Json;
namespace osu.Game.Users
@ -17,7 +16,7 @@ namespace osu.Game.Users
[JsonProperty("image")]
public string ImageLowRes = null!;
// TODO: remove when api returns @2x image link: https://github.com/ppy/osu-web/issues/9816
public string Image => $@"{Path.ChangeExtension(ImageLowRes, null)}@2x{Path.GetExtension(ImageLowRes)}";
[JsonProperty("image@2x")]
public string Image = null!;
}
}

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.20.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.314.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.320.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.403.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.402.0" />
<PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" />

View File

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

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.
#nullable disable
using System.Threading.Tasks;
using Foundation;
using osu.Framework.iOS;
using UIKit;
namespace osu.iOS
{
[Register("AppDelegate")]
public class AppDelegate : GameAppDelegate
{
private OsuGameIOS game;
protected override Framework.Game CreateGame() => game = new OsuGameIOS();
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
if (url.IsFileUrl)
Task.Run(() => game.Import(url.Path));
else
Task.Run(() => game.HandleLink(url.AbsoluteString));
return true;
}
}
}

View File

@ -1,9 +1,7 @@
// 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.
#nullable disable
using UIKit;
using osu.Framework.iOS;
namespace osu.iOS
{
@ -11,7 +9,7 @@ namespace osu.iOS
{
public static void Main(string[] args)
{
UIApplication.Main(args, null, typeof(AppDelegate));
GameApplication.Main(new OsuGameIOS());
}
}
}

View File

@ -1,36 +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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
namespace osu.iOS
{
public partial class IOSMouseSettings : SettingsSubsection
{
protected override LocalisableString Header => MouseSettingsStrings.Mouse;
[BackgroundDependencyLoader]
private void load(OsuConfigManager osuConfig)
{
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseWheelVolumeAdjust,
TooltipText = MouseSettingsStrings.DisableMouseWheelVolumeAdjustTooltip,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel),
},
new SettingsCheckbox
{
LabelText = MouseSettingsStrings.DisableMouseButtons,
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons),
},
};
}
}
}

View File

@ -7,10 +7,7 @@ using System;
using Foundation;
using Microsoft.Maui.Devices;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers;
using osu.Framework.iOS.Input;
using osu.Game;
using osu.Game.Overlays.Settings;
using osu.Game.Updater;
using osu.Game.Utils;
@ -29,18 +26,6 @@ namespace osu.iOS
// Because we have the home indicator (mostly) hidden we don't really care about drawing in this region.
Edges.Bottom;
public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
{
switch (handler)
{
case IOSMouseHandler:
return new IOSMouseSettings();
default:
return base.CreateSettingsSubsectionFor(handler);
}
}
private class IOSBatteryInfo : BatteryInfo
{
public override double? ChargeLevel => Battery.ChargeLevel;