Merge branch 'master' into volume-control-acceleration

This commit is contained in:
smoogipoo
2021-04-15 23:23:23 +09:00
68 changed files with 1002 additions and 800 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.413.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.415.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -7,6 +7,8 @@ using Android.OS;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game; using osu.Game;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils;
using Xamarin.Essentials;
namespace osu.Android namespace osu.Android
{ {
@ -72,5 +74,14 @@ namespace osu.Android
} }
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
protected override BatteryInfo CreateBatteryInfo() => new AndroidBatteryInfo();
private class AndroidBatteryInfo : BatteryInfo
{
public override double ChargeLevel => Battery.ChargeLevel;
public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery;
}
} }
} }

View File

@ -6,5 +6,6 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" /> <uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BATTERY_STATS" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" /> <application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
</manifest> </manifest>

View File

@ -63,5 +63,8 @@
<Version>5.0.0</Version> <Version>5.0.0</Version>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project> </Project>

View File

@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Mirror"; public override string Name => "Mirror";
public override string Acronym => "MR"; public override string Acronym => "MR";
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override string Description => "Notes are flipped horizontally.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool Ranked => true; public override bool Ranked => true;

View File

@ -0,0 +1,44 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModBarrelRoll : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
[SettingSource("Roll speed", "Rotations per minute")]
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
{
MinValue = 0.02,
MaxValue = 4,
Precision = 0.01,
};
[SettingSource("Direction", "The direction of rotation")]
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
public override string Name => "Barrel Roll";
public override string Acronym => "BR";
public override string Description => "The whole playfield is on a wheel!";
public override double ScoreMultiplier => 1;
public void Update(Playfield playfield)
{
playfield.Rotation = (Direction.Value == RotationDirection.CounterClockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// scale the playfield to allow all hitobjects to stay within the visible region.
drawableRuleset.Playfield.Scale = new Vector2(OsuPlayfield.BASE_SIZE.Y / OsuPlayfield.BASE_SIZE.X);
}
}
}

View File

@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public override string Name => "Touch Device"; public override string Name => "Touch Device";
public override string Acronym => "TD"; public override string Acronym => "TD";
public override string Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System; public override ModType Type => ModType.System;

View File

@ -185,6 +185,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModGrow(), new OsuModDeflate()), new MultiMod(new OsuModGrow(), new OsuModDeflate()),
new MultiMod(new ModWindUp(), new ModWindDown()), new MultiMod(new ModWindUp(), new ModWindDown()),
new OsuModTraceable(), new OsuModTraceable(),
new OsuModBarrelRoll(),
}; };
case ModType.System: case ModType.System:

View File

@ -42,6 +42,9 @@ namespace osu.Game.Rulesets.Osu.UI
public OsuPlayfield() public OsuPlayfield()
{ {
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },

View File

@ -33,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
Add(cursorScaleContainer = new Container Add(cursorScaleContainer = new Container
{ {
RelativePositionAxes = Axes.Both,
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume } Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
}); });
} }

View File

@ -144,6 +144,7 @@ namespace osu.Game.Tests.NonVisual
{ {
public override string Name => nameof(ModA); public override string Name => nameof(ModA);
public override string Acronym => nameof(ModA); public override string Acronym => nameof(ModA);
public override string Description => string.Empty;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) }; public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) };
@ -152,6 +153,7 @@ namespace osu.Game.Tests.NonVisual
private class ModB : Mod private class ModB : Mod
{ {
public override string Name => nameof(ModB); public override string Name => nameof(ModB);
public override string Description => string.Empty;
public override string Acronym => nameof(ModB); public override string Acronym => nameof(ModB);
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
@ -162,6 +164,7 @@ namespace osu.Game.Tests.NonVisual
{ {
public override string Name => nameof(ModC); public override string Name => nameof(ModC);
public override string Acronym => nameof(ModC); public override string Acronym => nameof(ModC);
public override string Description => string.Empty;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
} }
@ -169,6 +172,7 @@ namespace osu.Game.Tests.NonVisual
{ {
public override string Name => $"Incompatible With {nameof(ModA)}"; public override string Name => $"Incompatible With {nameof(ModA)}";
public override string Acronym => $"Incompatible With {nameof(ModA)}"; public override string Acronym => $"Incompatible With {nameof(ModA)}";
public override string Description => string.Empty;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModA) }; public override Type[] IncompatibleMods => new[] { typeof(ModA) };
@ -187,6 +191,7 @@ namespace osu.Game.Tests.NonVisual
{ {
public override string Name => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}"; public override string Name => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
public override string Acronym => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}"; public override string Acronym => $"Incompatible With {nameof(ModA)} and {nameof(ModB)}";
public override string Description => string.Empty;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) }; public override Type[] IncompatibleMods => new[] { typeof(ModA), typeof(ModB) };

View File

@ -20,27 +20,14 @@ namespace osu.Game.Tests.NonVisual
{ {
handler = new TestInputHandler(replay = new Replay handler = new TestInputHandler(replay = new Replay
{ {
Frames = new List<ReplayFrame> HasReceivedAllFrames = false
{
new TestReplayFrame(0),
new TestReplayFrame(1000),
new TestReplayFrame(2000),
new TestReplayFrame(3000, true),
new TestReplayFrame(4000, true),
new TestReplayFrame(5000, true),
new TestReplayFrame(7000, true),
new TestReplayFrame(8000),
}
}); });
} }
[Test] [Test]
public void TestNormalPlayback() public void TestNormalPlayback()
{ {
Assert.IsNull(handler.CurrentFrame); setReplayFrames();
confirmCurrentFrame(null);
confirmNextFrame(0);
setTime(0, 0); setTime(0, 0);
confirmCurrentFrame(0); confirmCurrentFrame(0);
@ -107,6 +94,8 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestIntroTime() public void TestIntroTime()
{ {
setReplayFrames();
setTime(-1000, -1000); setTime(-1000, -1000);
confirmCurrentFrame(null); confirmCurrentFrame(null);
confirmNextFrame(0); confirmNextFrame(0);
@ -123,6 +112,8 @@ namespace osu.Game.Tests.NonVisual
[Test] [Test]
public void TestBasicRewind() public void TestBasicRewind()
{ {
setReplayFrames();
setTime(2800, 0); setTime(2800, 0);
setTime(2800, 1000); setTime(2800, 1000);
setTime(2800, 2000); setTime(2800, 2000);
@ -133,34 +124,35 @@ namespace osu.Game.Tests.NonVisual
// pivot without crossing a frame boundary // pivot without crossing a frame boundary
setTime(2700, 2700); setTime(2700, 2700);
confirmCurrentFrame(2); confirmCurrentFrame(2);
confirmNextFrame(1); confirmNextFrame(3);
// cross current frame boundary; should not yet update frame // cross current frame boundary
setTime(1980, 1980); setTime(1980, 2000);
confirmCurrentFrame(2); confirmCurrentFrame(2);
confirmNextFrame(1); confirmNextFrame(3);
setTime(1200, 1200); setTime(1200, 1200);
confirmCurrentFrame(2); confirmCurrentFrame(1);
confirmNextFrame(1); confirmNextFrame(2);
// ensure each frame plays out until start // ensure each frame plays out until start
setTime(-500, 1000); setTime(-500, 1000);
confirmCurrentFrame(1); confirmCurrentFrame(1);
confirmNextFrame(0); confirmNextFrame(2);
setTime(-500, 0); setTime(-500, 0);
confirmCurrentFrame(0); confirmCurrentFrame(0);
confirmNextFrame(null); confirmNextFrame(1);
setTime(-500, -500); setTime(-500, -500);
confirmCurrentFrame(0); confirmCurrentFrame(null);
confirmNextFrame(null); confirmNextFrame(0);
} }
[Test] [Test]
public void TestRewindInsideImportantSection() public void TestRewindInsideImportantSection()
{ {
setReplayFrames();
fastForwardToPoint(3000); fastForwardToPoint(3000);
setTime(4000, 4000); setTime(4000, 4000);
@ -168,12 +160,12 @@ namespace osu.Game.Tests.NonVisual
confirmNextFrame(5); confirmNextFrame(5);
setTime(3500, null); setTime(3500, null);
confirmCurrentFrame(4); confirmCurrentFrame(3);
confirmNextFrame(3); confirmNextFrame(4);
setTime(3000, 3000); setTime(3000, 3000);
confirmCurrentFrame(3); confirmCurrentFrame(3);
confirmNextFrame(2); confirmNextFrame(4);
setTime(3500, null); setTime(3500, null);
confirmCurrentFrame(3); confirmCurrentFrame(3);
@ -187,46 +179,127 @@ namespace osu.Game.Tests.NonVisual
confirmCurrentFrame(4); confirmCurrentFrame(4);
confirmNextFrame(5); confirmNextFrame(5);
setTime(4000, null); setTime(4000, 4000);
confirmCurrentFrame(4); confirmCurrentFrame(4);
confirmNextFrame(5); confirmNextFrame(5);
setTime(3500, null); setTime(3500, null);
confirmCurrentFrame(4); confirmCurrentFrame(3);
confirmNextFrame(3); confirmNextFrame(4);
setTime(3000, 3000); setTime(3000, 3000);
confirmCurrentFrame(3); confirmCurrentFrame(3);
confirmNextFrame(2); confirmNextFrame(4);
} }
[Test] [Test]
public void TestRewindOutOfImportantSection() public void TestRewindOutOfImportantSection()
{ {
setReplayFrames();
fastForwardToPoint(3500); fastForwardToPoint(3500);
confirmCurrentFrame(3); confirmCurrentFrame(3);
confirmNextFrame(4); confirmNextFrame(4);
setTime(3200, null); setTime(3200, null);
// next frame doesn't change even though direction reversed, because of important section.
confirmCurrentFrame(3); confirmCurrentFrame(3);
confirmNextFrame(4); confirmNextFrame(4);
setTime(3000, null); setTime(3000, 3000);
confirmCurrentFrame(3); confirmCurrentFrame(3);
confirmNextFrame(4); confirmNextFrame(4);
setTime(2800, 2800); setTime(2800, 2800);
confirmCurrentFrame(3); confirmCurrentFrame(2);
confirmNextFrame(2); confirmNextFrame(3);
}
[Test]
public void TestReplayStreaming()
{
// no frames are arrived yet
setTime(0, null);
setTime(1000, null);
Assert.IsTrue(handler.WaitingForFrame, "Should be waiting for the first frame");
replay.Frames.Add(new TestReplayFrame(0));
replay.Frames.Add(new TestReplayFrame(1000));
// should always play from beginning
setTime(1000, 0);
confirmCurrentFrame(0);
Assert.IsFalse(handler.WaitingForFrame, "Should not be waiting yet");
setTime(1000, 1000);
confirmCurrentFrame(1);
confirmNextFrame(null);
Assert.IsTrue(handler.WaitingForFrame, "Should be waiting");
// cannot seek beyond the last frame
setTime(1500, null);
confirmCurrentFrame(1);
setTime(-100, 0);
confirmCurrentFrame(0);
// can seek to the point before the first frame, however
setTime(-100, -100);
confirmCurrentFrame(null);
confirmNextFrame(0);
fastForwardToPoint(1000);
setTime(3000, null);
replay.Frames.Add(new TestReplayFrame(2000));
confirmCurrentFrame(1);
setTime(1000, 1000);
setTime(3000, 2000);
}
[Test]
public void TestMultipleFramesSameTime()
{
replay.Frames.Add(new TestReplayFrame(0));
replay.Frames.Add(new TestReplayFrame(0));
replay.Frames.Add(new TestReplayFrame(1000));
replay.Frames.Add(new TestReplayFrame(1000));
replay.Frames.Add(new TestReplayFrame(2000));
// forward direction is prioritized when multiple frames have the same time.
setTime(0, 0);
setTime(0, 0);
setTime(2000, 1000);
setTime(2000, 1000);
setTime(1000, 1000);
setTime(1000, 1000);
setTime(-100, 1000);
setTime(-100, 0);
setTime(-100, 0);
setTime(-100, -100);
}
private void setReplayFrames()
{
replay.Frames = new List<ReplayFrame>
{
new TestReplayFrame(0),
new TestReplayFrame(1000),
new TestReplayFrame(2000),
new TestReplayFrame(3000, true),
new TestReplayFrame(4000, true),
new TestReplayFrame(5000, true),
new TestReplayFrame(7000, true),
new TestReplayFrame(8000),
};
replay.HasReceivedAllFrames = true;
} }
private void fastForwardToPoint(double destination) private void fastForwardToPoint(double destination)
{ {
for (int i = 0; i < 1000; i++) for (int i = 0; i < 1000; i++)
{ {
if (handler.SetFrameFromTime(destination) == null) var time = handler.SetFrameFromTime(destination);
if (time == null || time == destination)
return; return;
} }
@ -235,33 +308,17 @@ namespace osu.Game.Tests.NonVisual
private void setTime(double set, double? expect) private void setTime(double set, double? expect)
{ {
Assert.AreEqual(expect, handler.SetFrameFromTime(set)); Assert.AreEqual(expect, handler.SetFrameFromTime(set), "Unexpected return value");
} }
private void confirmCurrentFrame(int? frame) private void confirmCurrentFrame(int? frame)
{ {
if (frame.HasValue) Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.CurrentFrame?.Time, "Unexpected current frame");
{
Assert.IsNotNull(handler.CurrentFrame);
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
}
else
{
Assert.IsNull(handler.CurrentFrame);
}
} }
private void confirmNextFrame(int? frame) private void confirmNextFrame(int? frame)
{ {
if (frame.HasValue) Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.NextFrame?.Time, "Unexpected next frame");
{
Assert.IsNotNull(handler.NextFrame);
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
}
else
{
Assert.IsNull(handler.NextFrame);
}
} }
private class TestReplayFrame : ReplayFrame private class TestReplayFrame : ReplayFrame

View File

@ -1,296 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Replays;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class StreamingFramedReplayInputHandlerTest
{
private Replay replay;
private TestInputHandler handler;
[SetUp]
public void SetUp()
{
handler = new TestInputHandler(replay = new Replay
{
HasReceivedAllFrames = false,
Frames = new List<ReplayFrame>
{
new TestReplayFrame(0),
new TestReplayFrame(1000),
new TestReplayFrame(2000),
new TestReplayFrame(3000, true),
new TestReplayFrame(4000, true),
new TestReplayFrame(5000, true),
new TestReplayFrame(7000, true),
new TestReplayFrame(8000),
}
});
}
[Test]
public void TestNormalPlayback()
{
Assert.IsNull(handler.CurrentFrame);
confirmCurrentFrame(null);
confirmNextFrame(0);
setTime(0, 0);
confirmCurrentFrame(0);
confirmNextFrame(1);
// if we hit the first frame perfectly, time should progress to it.
setTime(1000, 1000);
confirmCurrentFrame(1);
confirmNextFrame(2);
// in between non-important frames should progress based on input.
setTime(1200, 1200);
confirmCurrentFrame(1);
setTime(1400, 1400);
confirmCurrentFrame(1);
// progressing beyond the next frame should force time to that frame once.
setTime(2200, 2000);
confirmCurrentFrame(2);
// second attempt should progress to input time
setTime(2200, 2200);
confirmCurrentFrame(2);
// entering important section
setTime(3000, 3000);
confirmCurrentFrame(3);
// cannot progress within
setTime(3500, null);
confirmCurrentFrame(3);
setTime(4000, 4000);
confirmCurrentFrame(4);
// still cannot progress
setTime(4500, null);
confirmCurrentFrame(4);
setTime(5200, 5000);
confirmCurrentFrame(5);
// important section AllowedImportantTimeSpan allowance
setTime(5200, 5200);
confirmCurrentFrame(5);
setTime(7200, 7000);
confirmCurrentFrame(6);
setTime(7200, null);
confirmCurrentFrame(6);
// exited important section
setTime(8200, 8000);
confirmCurrentFrame(7);
confirmNextFrame(null);
setTime(8200, null);
confirmCurrentFrame(7);
confirmNextFrame(null);
setTime(8400, null);
confirmCurrentFrame(7);
confirmNextFrame(null);
}
[Test]
public void TestIntroTime()
{
setTime(-1000, -1000);
confirmCurrentFrame(null);
confirmNextFrame(0);
setTime(-500, -500);
confirmCurrentFrame(null);
confirmNextFrame(0);
setTime(0, 0);
confirmCurrentFrame(0);
confirmNextFrame(1);
}
[Test]
public void TestBasicRewind()
{
setTime(2800, 0);
setTime(2800, 1000);
setTime(2800, 2000);
setTime(2800, 2800);
confirmCurrentFrame(2);
confirmNextFrame(3);
// pivot without crossing a frame boundary
setTime(2700, 2700);
confirmCurrentFrame(2);
confirmNextFrame(1);
// cross current frame boundary; should not yet update frame
setTime(1980, 1980);
confirmCurrentFrame(2);
confirmNextFrame(1);
setTime(1200, 1200);
confirmCurrentFrame(2);
confirmNextFrame(1);
// ensure each frame plays out until start
setTime(-500, 1000);
confirmCurrentFrame(1);
confirmNextFrame(0);
setTime(-500, 0);
confirmCurrentFrame(0);
confirmNextFrame(null);
setTime(-500, -500);
confirmCurrentFrame(0);
confirmNextFrame(null);
}
[Test]
public void TestRewindInsideImportantSection()
{
fastForwardToPoint(3000);
setTime(4000, 4000);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(3500, null);
confirmCurrentFrame(4);
confirmNextFrame(3);
setTime(3000, 3000);
confirmCurrentFrame(3);
confirmNextFrame(2);
setTime(3500, null);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(4000, 4000);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(4500, null);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(4000, null);
confirmCurrentFrame(4);
confirmNextFrame(5);
setTime(3500, null);
confirmCurrentFrame(4);
confirmNextFrame(3);
setTime(3000, 3000);
confirmCurrentFrame(3);
confirmNextFrame(2);
}
[Test]
public void TestRewindOutOfImportantSection()
{
fastForwardToPoint(3500);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(3200, null);
// next frame doesn't change even though direction reversed, because of important section.
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(3000, null);
confirmCurrentFrame(3);
confirmNextFrame(4);
setTime(2800, 2800);
confirmCurrentFrame(3);
confirmNextFrame(2);
}
private void fastForwardToPoint(double destination)
{
for (int i = 0; i < 1000; i++)
{
if (handler.SetFrameFromTime(destination) == null)
return;
}
throw new TimeoutException("Seek was never fulfilled");
}
private void setTime(double set, double? expect)
{
Assert.AreEqual(expect, handler.SetFrameFromTime(set));
}
private void confirmCurrentFrame(int? frame)
{
if (frame.HasValue)
{
Assert.IsNotNull(handler.CurrentFrame);
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
}
else
{
Assert.IsNull(handler.CurrentFrame);
}
}
private void confirmNextFrame(int? frame)
{
if (frame.HasValue)
{
Assert.IsNotNull(handler.NextFrame);
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
}
else
{
Assert.IsNull(handler.NextFrame);
}
}
private class TestReplayFrame : ReplayFrame
{
public readonly bool IsImportant;
public TestReplayFrame(double time, bool isImportant = false)
: base(time)
{
IsImportant = isImportant;
}
}
private class TestInputHandler : FramedReplayInputHandler<TestReplayFrame>
{
public TestInputHandler(Replay replay)
: base(replay)
{
FrameAccuratePlayback = true;
}
protected override double AllowedImportantTimeSpan => 1000;
protected override bool IsImportant(TestReplayFrame frame) => frame.IsImportant;
}
}
}

View File

@ -140,6 +140,7 @@ namespace osu.Game.Tests.Online
{ {
public override string Name => "Test Mod"; public override string Name => "Test Mod";
public override string Acronym => "TM"; public override string Acronym => "TM";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
[SettingSource("Test")] [SettingSource("Test")]
@ -156,6 +157,7 @@ namespace osu.Game.Tests.Online
{ {
public override string Name => "Test Mod"; public override string Name => "Test Mod";
public override string Acronym => "TMTR"; public override string Acronym => "TMTR";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")] [SettingSource("Initial rate", "The starting speed of the track")]

View File

@ -100,6 +100,7 @@ namespace osu.Game.Tests.Online
{ {
public override string Name => "Test Mod"; public override string Name => "Test Mod";
public override string Acronym => "TM"; public override string Acronym => "TM";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
[SettingSource("Test")] [SettingSource("Test")]
@ -116,6 +117,7 @@ namespace osu.Game.Tests.Online
{ {
public override string Name => "Test Mod"; public override string Name => "Test Mod";
public override string Acronym => "TMTR"; public override string Acronym => "TMTR";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")] [SettingSource("Initial rate", "The starting speed of the track")]
@ -150,6 +152,7 @@ namespace osu.Game.Tests.Online
{ {
public override string Name => "Test Mod"; public override string Name => "Test Mod";
public override string Acronym => "TM"; public override string Acronym => "TM";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
[SettingSource("Test")] [SettingSource("Test")]

View File

@ -65,6 +65,21 @@ namespace osu.Game.Tests.Visual.Background
stack.Push(songSelect = new DummySongSelect()); stack.Push(songSelect = new DummySongSelect());
}); });
/// <summary>
/// User settings should always be ignored on song select screen.
/// </summary>
[Test]
public void TestUserSettingsIgnoredOnSongSelect()
{
setupUserSettings();
AddUntilStep("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
AddUntilStep("Screen using background blur", () => songSelect.IsBackgroundBlur());
performFullSetup();
AddStep("Exit to song select", () => player.Exit());
AddUntilStep("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
AddUntilStep("Screen using background blur", () => songSelect.IsBackgroundBlur());
}
/// <summary> /// <summary>
/// Check if <see cref="PlayerLoader"/> properly triggers the visual settings preview when a user hovers over the visual settings panel. /// Check if <see cref="PlayerLoader"/> properly triggers the visual settings preview when a user hovers over the visual settings panel.
/// </summary> /// </summary>
@ -142,9 +157,9 @@ namespace osu.Game.Tests.Visual.Background
{ {
performFullSetup(); performFullSetup();
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false); AddStep("Disable user dim", () => songSelect.IgnoreUserSettings.Value = true);
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled()); AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true); AddStep("Enable user dim", () => songSelect.IgnoreUserSettings.Value = false);
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
} }
@ -161,13 +176,36 @@ namespace osu.Game.Tests.Visual.Background
player.ReplacesBackground.Value = true; player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true; player.StoryboardEnabled.Value = true;
}); });
AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true); AddStep("Enable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = false);
AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f); AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f);
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible); AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false); AddStep("Disable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = true);
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible); AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
} }
[Test]
public void TestStoryboardIgnoreUserSettings()
{
performFullSetup();
createFakeStoryboard();
AddStep("Enable replacing background", () => player.ReplacesBackground.Value = true);
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
AddStep("Ignore user settings", () =>
{
player.ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
});
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
AddUntilStep("Background is invisible", () => songSelect.IsBackgroundInvisible());
AddStep("Disable background replacement", () => player.ReplacesBackground.Value = false);
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
}
/// <summary> /// <summary>
/// Check if the visual settings container retains dim and blur when pausing /// Check if the visual settings container retains dim and blur when pausing
/// </summary> /// </summary>
@ -204,17 +242,6 @@ namespace osu.Game.Tests.Visual.Background
songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && songSelect.CheckBackgroundBlur(results.ExpectedBackgroundBlur)); songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && songSelect.CheckBackgroundBlur(results.ExpectedBackgroundBlur));
} }
/// <summary>
/// Check if background gets undimmed and unblurred when leaving <see cref="Player"/> for <see cref="PlaySongSelect"/>
/// </summary>
[Test]
public void TestTransitionOut()
{
performFullSetup();
AddStep("Exit to song select", () => player.Exit());
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect());
}
/// <summary> /// <summary>
/// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim. /// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim.
/// </summary> /// </summary>
@ -281,11 +308,11 @@ namespace osu.Game.Tests.Visual.Background
protected override BackgroundScreen CreateBackground() protected override BackgroundScreen CreateBackground()
{ {
background = new FadeAccessibleBackground(Beatmap.Value); background = new FadeAccessibleBackground(Beatmap.Value);
DimEnabled.BindTo(background.EnableUserDim); IgnoreUserSettings.BindTo(background.IgnoreUserSettings);
return background; return background;
} }
public readonly Bindable<bool> DimEnabled = new Bindable<bool>(); public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>();
public readonly Bindable<double> DimLevel = new BindableDouble(); public readonly Bindable<double> DimLevel = new BindableDouble();
public readonly Bindable<double> BlurLevel = new BindableDouble(); public readonly Bindable<double> BlurLevel = new BindableDouble();
@ -310,7 +337,7 @@ namespace osu.Game.Tests.Visual.Background
public bool IsBackgroundVisible() => background.CurrentAlpha == 1; public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
public bool IsBlurCorrect() => background.CurrentBlur == new Vector2(BACKGROUND_BLUR); public bool IsBackgroundBlur() => background.CurrentBlur == new Vector2(BACKGROUND_BLUR);
public bool CheckBackgroundBlur(Vector2 expected) => background.CurrentBlur == expected; public bool CheckBackgroundBlur(Vector2 expected) => background.CurrentBlur == expected;

View File

@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osuTK; using osuTK;
@ -16,18 +15,28 @@ namespace osu.Game.Tests.Visual.Editing
public class TestSceneEditorSummaryTimeline : EditorClockTestScene public class TestSceneEditorSummaryTimeline : EditorClockTestScene
{ {
[Cached(typeof(EditorBeatmap))] [Cached(typeof(EditorBeatmap))]
private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap()); private readonly EditorBeatmap editorBeatmap;
[BackgroundDependencyLoader] public TestSceneEditorSummaryTimeline()
private void load()
{ {
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
}
Add(new SummaryTimeline protected override void LoadComplete()
{
base.LoadComplete();
AddStep("create timeline", () =>
{ {
Anchor = Anchor.Centre, // required for track
Origin = Anchor.Centre, Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Size = new Vector2(500, 50)
Add(new SummaryTimeline
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 50)
});
}); });
} }
} }

View File

@ -53,13 +53,10 @@ namespace osu.Game.Tests.Visual.Editing
new AudioVisualiser(), new AudioVisualiser(),
} }
}, },
TimelineArea = new TimelineArea TimelineArea = new TimelineArea(CreateTestComponent())
{ {
Child = CreateTestComponent(),
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Size = new Vector2(0.8f, 100),
} }
}); });
} }

View File

@ -25,6 +25,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Utils;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -48,6 +49,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached] [Cached]
private readonly VolumeOverlay volumeOverlay; private readonly VolumeOverlay volumeOverlay;
[Cached(typeof(BatteryInfo))]
private readonly LocalBatteryInfo batteryInfo = new LocalBatteryInfo();
private readonly ChangelogOverlay changelogOverlay; private readonly ChangelogOverlay changelogOverlay;
public TestScenePlayerLoader() public TestScenePlayerLoader()
@ -288,6 +292,33 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
} }
[TestCase(false, 1.0, false)] // not charging, above cutoff --> no warning
[TestCase(true, 0.1, false)] // charging, below cutoff --> no warning
[TestCase(false, 0.25, true)] // not charging, at cutoff --> warning
public void TestLowBatteryNotification(bool isCharging, double chargeLevel, bool shouldWarn)
{
AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce).Value = false);
// set charge status and level
AddStep("load player", () => resetPlayer(false, () =>
{
batteryInfo.SetCharging(isCharging);
batteryInfo.SetChargeLevel(chargeLevel);
}));
AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
AddAssert($"notification {(shouldWarn ? "triggered" : "not triggered")}", () => notificationOverlay.UnreadCount.Value == (shouldWarn ? 1 : 0));
AddStep("click notification", () =>
{
var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last();
var flowContainer = scrollContainer.Children.OfType<FillFlowContainer<NotificationSection>>().First();
var notification = flowContainer.First();
InputManager.MoveMouseTo(notification);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for player load", () => player.IsLoaded);
}
[Test] [Test]
public void TestEpilepsyWarningEarlyExit() public void TestEpilepsyWarningEarlyExit()
{ {
@ -321,6 +352,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override string Name => string.Empty; public override string Name => string.Empty;
public override string Acronym => string.Empty; public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override string Description => string.Empty;
public bool Applied { get; private set; } public bool Applied { get; private set; }
@ -348,5 +380,29 @@ namespace osu.Game.Tests.Visual.Gameplay
throw new TimeoutException(); throw new TimeoutException();
} }
} }
/// <summary>
/// Mutable dummy BatteryInfo class for <see cref="TestScenePlayerLoader.TestLowBatteryNotification"/>
/// </summary>
/// <inheritdoc/>
private class LocalBatteryInfo : BatteryInfo
{
private bool isCharging = true;
private double chargeLevel = 1;
public override bool IsCharging => isCharging;
public override double ChargeLevel => chargeLevel;
public void SetCharging(bool value)
{
isCharging = value;
}
public void SetChargeLevel(double value)
{
chargeLevel = value;
}
}
} }
} }

View File

@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
waitForPlayer(); waitForPlayer();
AddAssert("ensure frames arrived", () => replayHandler.HasFrames); AddAssert("ensure frames arrived", () => replayHandler.HasFrames);
AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame);
checkPaused(true); checkPaused(true);
double? pausedTime = null; double? pausedTime = null;
@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay
sendFrames(); sendFrames();
AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame);
checkPaused(true); checkPaused(true);
AddAssert("time advanced", () => currentFrameStableTime > pausedTime); AddAssert("time advanced", () => currentFrameStableTime > pausedTime);

View File

@ -204,27 +204,27 @@ namespace osu.Game.Tests.Visual.Gameplay
return; return;
} }
if (replayHandler.NextFrame != null) if (!replayHandler.HasFrames)
{ return;
var lastFrame = replay.Frames.LastOrDefault();
// this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved). var lastFrame = replay.Frames.LastOrDefault();
// in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation.
if (lastFrame != null)
latency = Math.Max(latency, Time.Current - lastFrame.Time);
latencyDisplay.Text = $"latency: {latency:N1}"; // this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved).
// in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation.
if (lastFrame != null)
latency = Math.Max(latency, Time.Current - lastFrame.Time);
double proposedTime = Time.Current - latency + Time.Elapsed; latencyDisplay.Text = $"latency: {latency:N1}";
// this will either advance by one or zero frames. double proposedTime = Time.Current - latency + Time.Elapsed;
double? time = replayHandler.SetFrameFromTime(proposedTime);
if (time == null) // this will either advance by one or zero frames.
return; double? time = replayHandler.SetFrameFromTime(proposedTime);
manualClock.CurrentTime = time.Value; if (time == null)
} return;
manualClock.CurrentTime = time.Value;
} }
[TearDownSteps] [TearDownSteps]

View File

@ -57,6 +57,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private abstract class TestMod : Mod, IApplicableMod private abstract class TestMod : Mod, IApplicableMod
{ {
public override double ScoreMultiplier => 1.0; public override double ScoreMultiplier => 1.0;
public override string Description => "This is a test mod.";
} }
} }
} }

View File

@ -226,6 +226,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public override double ScoreMultiplier => 1.0; public override double ScoreMultiplier => 1.0;
public override string Description => "This is a customisable test mod.";
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
[SettingSource("Sample float", "Change something for a mod")] [SettingSource("Sample float", "Change something for a mod")]

View File

@ -22,4 +22,4 @@
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.ControlPoints
MaxValue = 10 MaxValue = 10
}; };
public override Color4 GetRepresentingColour(OsuColour colours) => colours.GreenDark; public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
/// <summary> /// <summary>
/// The speed multiplier at this control point. /// The speed multiplier at this control point.

View File

@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
private const double default_beat_length = 60000.0 / 60.0; private const double default_beat_length = 60000.0 / 60.0;
public override Color4 GetRepresentingColour(OsuColour colours) => colours.YellowDark; public override Color4 GetRepresentingColour(OsuColour colours) => colours.Orange1;
public static readonly TimingControlPoint DEFAULT = new TimingControlPoint public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
{ {

View File

@ -16,6 +16,7 @@ namespace osu.Game.Configuration
{ {
SetDefault(Static.LoginOverlayDisplayed, false); SetDefault(Static.LoginOverlayDisplayed, false);
SetDefault(Static.MutedAudioNotificationShownOnce, false); SetDefault(Static.MutedAudioNotificationShownOnce, false);
SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null); SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
} }
@ -25,6 +26,7 @@ namespace osu.Game.Configuration
{ {
LoginOverlayDisplayed, LoginOverlayDisplayed,
MutedAudioNotificationShownOnce, MutedAudioNotificationShownOnce,
LowBatteryNotificationShownOnce,
/// <summary> /// <summary>
/// Info about seasonal backgrounds available fetched from API - see <see cref="APISeasonalBackgrounds"/>. /// Info about seasonal backgrounds available fetched from API - see <see cref="APISeasonalBackgrounds"/>.

View File

@ -9,6 +9,9 @@ namespace osu.Game.Extensions
{ {
public static class DrawableExtensions public static class DrawableExtensions
{ {
public const double REPEAT_INTERVAL = 70;
public const double INITIAL_DELAY = 250;
/// <summary> /// <summary>
/// Helper method that is used while <see cref="IKeyBindingHandler"/> doesn't support repetitions of <see cref="IKeyBindingHandler{T}.OnPressed"/>. /// Helper method that is used while <see cref="IKeyBindingHandler"/> doesn't support repetitions of <see cref="IKeyBindingHandler{T}.OnPressed"/>.
/// Simulates repetitions by continually invoking a delegate according to the default key repeat rate. /// Simulates repetitions by continually invoking a delegate according to the default key repeat rate.
@ -19,12 +22,13 @@ namespace osu.Game.Extensions
/// <param name="handler">The <see cref="IKeyBindingHandler{T}"/> which is handling the repeat.</param> /// <param name="handler">The <see cref="IKeyBindingHandler{T}"/> which is handling the repeat.</param>
/// <param name="scheduler">The <see cref="Scheduler"/> to schedule repetitions on.</param> /// <param name="scheduler">The <see cref="Scheduler"/> to schedule repetitions on.</param>
/// <param name="action">The <see cref="Action"/> to be invoked once immediately and with every repetition.</param> /// <param name="action">The <see cref="Action"/> to be invoked once immediately and with every repetition.</param>
/// <param name="initialRepeatDelay">The delay imposed on the first repeat. Defaults to <see cref="INITIAL_DELAY"/>.</param>
/// <returns>A <see cref="ScheduledDelegate"/> which can be cancelled to stop the repeat events from firing.</returns> /// <returns>A <see cref="ScheduledDelegate"/> which can be cancelled to stop the repeat events from firing.</returns>
public static ScheduledDelegate BeginKeyRepeat(this IKeyBindingHandler handler, Scheduler scheduler, Action action) public static ScheduledDelegate BeginKeyRepeat(this IKeyBindingHandler handler, Scheduler scheduler, Action action, double initialRepeatDelay = INITIAL_DELAY)
{ {
action(); action();
ScheduledDelegate repeatDelegate = new ScheduledDelegate(action, handler.Time.Current + 250, 70); ScheduledDelegate repeatDelegate = new ScheduledDelegate(action, handler.Time.Current + initialRepeatDelay, REPEAT_INTERVAL);
scheduler.Add(repeatDelegate); scheduler.Add(repeatDelegate);
return repeatDelegate; return repeatDelegate;
} }

View File

@ -23,11 +23,6 @@ namespace osu.Game.Graphics.Containers
protected const double BACKGROUND_FADE_DURATION = 800; protected const double BACKGROUND_FADE_DURATION = 800;
/// <summary>
/// Whether or not user-configured dim levels should be applied to the container.
/// </summary>
public readonly Bindable<bool> EnableUserDim = new Bindable<bool>(true);
/// <summary> /// <summary>
/// Whether or not user-configured settings relating to brightness of elements should be ignored /// Whether or not user-configured settings relating to brightness of elements should be ignored
/// </summary> /// </summary>
@ -57,7 +52,7 @@ namespace osu.Game.Graphics.Containers
private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0; private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0;
protected float DimLevel => Math.Max(EnableUserDim.Value && !IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : 0, 0); protected float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : 0, 0);
protected override Container<Drawable> Content => dimContent; protected override Container<Drawable> Content => dimContent;
@ -78,7 +73,6 @@ namespace osu.Game.Graphics.Containers
LightenDuringBreaks = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks); LightenDuringBreaks = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks);
ShowStoryboard = config.GetBindable<bool>(OsuSetting.ShowStoryboard); ShowStoryboard = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
EnableUserDim.ValueChanged += _ => UpdateVisuals();
UserDimLevel.ValueChanged += _ => UpdateVisuals(); UserDimLevel.ValueChanged += _ => UpdateVisuals();
LightenDuringBreaks.ValueChanged += _ => UpdateVisuals(); LightenDuringBreaks.ValueChanged += _ => UpdateVisuals();
IsBreakTime.ValueChanged += _ => UpdateVisuals(); IsBreakTime.ValueChanged += _ => UpdateVisuals();

View File

@ -186,6 +186,13 @@ namespace osu.Game.Graphics
public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee"); public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee");
public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff"); public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff");
// in latest editor design logic, need to figure out where these sit...
public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66");
public readonly Color4 Orange1 = Color4Extensions.FromHex(@"ffd966");
// Content Background
public readonly Color4 B5 = Color4Extensions.FromHex(@"222a28");
public readonly Color4 RedLighter = Color4Extensions.FromHex(@"ffeded"); public readonly Color4 RedLighter = Color4Extensions.FromHex(@"ffeded");
public readonly Color4 RedLight = Color4Extensions.FromHex(@"ed7787"); public readonly Color4 RedLight = Color4Extensions.FromHex(@"ed7787");
public readonly Color4 Red = Color4Extensions.FromHex(@"ed1121"); public readonly Color4 Red = Color4Extensions.FromHex(@"ed1121");

View File

@ -32,8 +32,6 @@ namespace osu.Game.Input.Handlers
public override bool Initialize(GameHost host) => true; public override bool Initialize(GameHost host) => true;
public override bool IsActive => true;
public class ReplayState<T> : IInput public class ReplayState<T> : IInput
where T : struct where T : struct
{ {

View File

@ -40,6 +40,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK.Input; using osuTK.Input;
using RuntimeInfo = osu.Framework.RuntimeInfo; using RuntimeInfo = osu.Framework.RuntimeInfo;
@ -156,6 +157,8 @@ namespace osu.Game
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
protected virtual BatteryInfo CreateBatteryInfo() => null;
/// <summary> /// <summary>
/// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects. /// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects.
/// </summary> /// </summary>
@ -281,6 +284,11 @@ namespace osu.Game
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));
var powerStatus = CreateBatteryInfo();
if (powerStatus != null)
dependencies.CacheAs(powerStatus);
dependencies.Cache(new SessionStatics()); dependencies.Cache(new SessionStatics());
dependencies.Cache(new OsuColour()); dependencies.Cache(new OsuColour());

View File

@ -6,6 +6,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Extensions;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Volume namespace osu.Game.Overlays.Volume
@ -15,8 +17,30 @@ namespace osu.Game.Overlays.Volume
public Func<GlobalAction, bool> ActionRequested; public Func<GlobalAction, bool> ActionRequested;
public Func<GlobalAction, float, bool, bool> ScrollActionRequested; public Func<GlobalAction, float, bool, bool> ScrollActionRequested;
public bool OnPressed(GlobalAction action) => private ScheduledDelegate keyRepeat;
ActionRequested?.Invoke(action) ?? false;
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.DecreaseVolume:
case GlobalAction.IncreaseVolume:
keyRepeat?.Cancel();
keyRepeat = this.BeginKeyRepeat(Scheduler, () => ActionRequested?.Invoke(action), 150);
return true;
case GlobalAction.ToggleMute:
ActionRequested?.Invoke(action);
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
keyRepeat?.Cancel();
}
protected override bool OnScroll(ScrollEvent e) protected override bool OnScroll(ScrollEvent e)
{ {
@ -27,9 +51,5 @@ namespace osu.Game.Overlays.Volume
public bool OnScroll(GlobalAction action, float amount, bool isPrecise) => public bool OnScroll(GlobalAction action, float amount, bool isPrecise) =>
ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false; ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false;
public void OnReleased(GlobalAction action)
{
}
} }
} }

View File

@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mods
/// The user readable description of this mod. /// The user readable description of this mod.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public virtual string Description => string.Empty; public abstract string Description { get; }
/// <summary> /// <summary>
/// The tooltip to display for this mod when used in a <see cref="ModIcon"/>. /// The tooltip to display for this mod when used in a <see cref="ModIcon"/>.

View File

@ -37,8 +37,7 @@ namespace osu.Game.Rulesets.Mods
public void ApplyToPlayer(Player player) public void ApplyToPlayer(Player player)
{ {
player.ApplyToBackground(b => b.EnableUserDim.Value = false); player.ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
player.DimmableStoryboard.IgnoreUserSettings.Value = true; player.DimmableStoryboard.IgnoreUserSettings.Value = true;
player.BreakOverlay.Hide(); player.BreakOverlay.Hide();

View File

@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Mods
{ {
public override string Name => "No Mod"; public override string Name => "No Mod";
public override string Acronym => "NM"; public override string Acronym => "NM";
public override string Description => "No mods applied.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Ban; public override IconUsage? Icon => FontAwesome.Solid.Ban;
public override ModType Type => ModType.System; public override ModType Type => ModType.System;

View File

@ -56,8 +56,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
public virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples; public virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;
private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>(); private readonly List<DrawableHitObject> nestedHitObjects = new List<DrawableHitObject>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<DrawableHitObject>(); public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects;
/// <summary> /// <summary>
/// Whether this object should handle any user input events. /// Whether this object should handle any user input events.
@ -249,7 +249,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
// Must be done before the nested DHO is added to occur before the nested Apply()! // Must be done before the nested DHO is added to occur before the nested Apply()!
drawableNested.ParentHitObject = this; drawableNested.ParentHitObject = this;
nestedHitObjects.Value.Add(drawableNested); nestedHitObjects.Add(drawableNested);
AddNestedHitObject(drawableNested); AddNestedHitObject(drawableNested);
} }
@ -305,19 +305,16 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (Samples != null) if (Samples != null)
Samples.Samples = null; Samples.Samples = null;
if (nestedHitObjects.IsValueCreated) foreach (var obj in nestedHitObjects)
{ {
foreach (var obj in nestedHitObjects.Value) obj.OnNewResult -= onNewResult;
{ obj.OnRevertResult -= onRevertResult;
obj.OnNewResult -= onNewResult; obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
obj.OnRevertResult -= onRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
nestedHitObjects.Value.Clear();
ClearNestedHitObjects();
} }
nestedHitObjects.Clear();
ClearNestedHitObjects();
HitObject.DefaultsApplied -= onDefaultsApplied; HitObject.DefaultsApplied -= onDefaultsApplied;
OnFree(); OnFree();

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
@ -17,80 +16,92 @@ namespace osu.Game.Rulesets.Replays
public abstract class FramedReplayInputHandler<TFrame> : ReplayInputHandler public abstract class FramedReplayInputHandler<TFrame> : ReplayInputHandler
where TFrame : ReplayFrame where TFrame : ReplayFrame
{ {
private readonly Replay replay; /// <summary>
/// Whether we have at least one replay frame.
/// </summary>
public bool HasFrames => Frames.Count != 0;
protected List<ReplayFrame> Frames => replay.Frames; /// <summary>
/// Whether we are waiting for new frames to be received.
/// </summary>
public bool WaitingForFrame => !replay.HasReceivedAllFrames && currentFrameIndex == Frames.Count - 1;
/// <summary>
/// The current frame of the replay.
/// The current time is always between the start and the end time of the current frame.
/// </summary>
/// <remarks>Returns null if the current time is strictly before the first frame.</remarks>
/// <exception cref="InvalidOperationException">The replay is empty.</exception>
public TFrame CurrentFrame public TFrame CurrentFrame
{ {
get get
{ {
if (!HasFrames || !currentFrameIndex.HasValue) if (!HasFrames)
return null; throw new InvalidOperationException($"Attempted to get {nameof(CurrentFrame)} of an empty replay");
return (TFrame)Frames[currentFrameIndex.Value]; return currentFrameIndex == -1 ? null : (TFrame)Frames[currentFrameIndex];
} }
} }
/// <summary>
/// The next frame of the replay.
/// The start time is always greater or equal to the start time of <see cref="CurrentFrame"/> regardless of the seeking direction.
/// </summary>
/// <remarks>Returns null if the current frame is the last frame.</remarks>
/// <exception cref="InvalidOperationException">The replay is empty.</exception>
public TFrame NextFrame public TFrame NextFrame
{ {
get get
{ {
if (!HasFrames) if (!HasFrames)
return null; throw new InvalidOperationException($"Attempted to get {nameof(NextFrame)} of an empty replay");
if (!currentFrameIndex.HasValue) return currentFrameIndex == Frames.Count - 1 ? null : (TFrame)Frames[currentFrameIndex + 1];
return currentDirection > 0 ? (TFrame)Frames[0] : null;
int nextFrame = clampedNextFrameIndex;
if (nextFrame == currentFrameIndex.Value)
return null;
return (TFrame)Frames[clampedNextFrameIndex];
} }
} }
private int? currentFrameIndex;
private int clampedNextFrameIndex =>
currentFrameIndex.HasValue ? Math.Clamp(currentFrameIndex.Value + currentDirection, 0, Frames.Count - 1) : 0;
protected FramedReplayInputHandler(Replay replay)
{
this.replay = replay;
}
private const double sixty_frame_time = 1000.0 / 60;
protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;
protected double? CurrentTime { get; private set; }
private int currentDirection = 1;
/// <summary> /// <summary>
/// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data. /// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data.
/// Disabling this can make replay playback smoother (useful for autoplay, currently). /// Disabling this can make replay playback smoother (useful for autoplay, currently).
/// </summary> /// </summary>
public bool FrameAccuratePlayback; public bool FrameAccuratePlayback;
public bool HasFrames => Frames.Count > 0; // This input handler should be enabled only if there is at least one replay frame.
public override bool IsActive => HasFrames;
// Can make it non-null but that is a breaking change.
protected double? CurrentTime { get; private set; }
protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;
protected List<ReplayFrame> Frames => replay.Frames;
private readonly Replay replay;
private int currentFrameIndex;
private const double sixty_frame_time = 1000.0 / 60;
protected FramedReplayInputHandler(Replay replay)
{
// TODO: This replay frame ordering should be enforced on the Replay type.
// Currently, the ordering can be broken if the frames are added after this construction.
replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time));
this.replay = replay;
currentFrameIndex = -1;
CurrentTime = double.NegativeInfinity;
}
private bool inImportantSection private bool inImportantSection
{ {
get get
{ {
if (!HasFrames || !FrameAccuratePlayback) if (!HasFrames || !FrameAccuratePlayback || CurrentFrame == null)
return false; return false;
var frame = currentDirection > 0 ? CurrentFrame : NextFrame; return IsImportant(CurrentFrame) && // a button is in a pressed state
Math.Abs(CurrentTime - NextFrame.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
if (frame == null)
return false;
return IsImportant(frame) && // a button is in a pressed state
Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
} }
} }
@ -105,71 +116,52 @@ namespace osu.Game.Rulesets.Replays
/// <returns>The usable time value. If null, we should not advance time as we do not have enough data.</returns> /// <returns>The usable time value. If null, we should not advance time as we do not have enough data.</returns>
public override double? SetFrameFromTime(double time) public override double? SetFrameFromTime(double time)
{ {
updateDirection(time);
Debug.Assert(currentDirection != 0);
if (!HasFrames) if (!HasFrames)
{ {
// in the case all frames are received, allow time to progress regardless. // In the case all frames are received, allow time to progress regardless.
if (replay.HasReceivedAllFrames) if (replay.HasReceivedAllFrames)
return CurrentTime = time; return CurrentTime = time;
return null; return null;
} }
TFrame next = NextFrame; double frameStart = getFrameTime(currentFrameIndex);
double frameEnd = getFrameTime(currentFrameIndex + 1);
// if we have a next frame, check if it is before or at the current time in playback, and advance time to it if so. // If the proposed time is after the current frame end time, we progress forwards to precisely the new frame's time (regardless of incoming time).
if (next != null) if (frameEnd <= time)
{ {
int compare = time.CompareTo(next.Time); time = frameEnd;
currentFrameIndex++;
if (compare == 0 || compare == currentDirection)
{
currentFrameIndex = clampedNextFrameIndex;
return CurrentTime = CurrentFrame.Time;
}
} }
// If the proposed time is before the current frame start time, and we are at the frame boundary, we progress backwards.
else if (time < frameStart && CurrentTime == frameStart)
currentFrameIndex--;
// at this point, the frame index can't be advanced. frameStart = getFrameTime(currentFrameIndex);
// even so, we may be able to propose the clock progresses forward due to being at an extent of the replay, frameEnd = getFrameTime(currentFrameIndex + 1);
// or moving towards the next valid frame (ie. interpolating in a non-important section).
// the exception is if currently in an important section, which is respected above all. // Pause until more frames are arrived.
if (inImportantSection) if (WaitingForFrame && frameStart < time)
{ {
Debug.Assert(next != null || !replay.HasReceivedAllFrames); CurrentTime = frameStart;
return null; return null;
} }
// if a next frame does exist, allow interpolation. CurrentTime = Math.Clamp(time, frameStart, frameEnd);
if (next != null)
return CurrentTime = time;
// if all frames have been received, allow playing beyond extents. // In an important section, a mid-frame time cannot be used and a null is returned instead.
if (replay.HasReceivedAllFrames) return inImportantSection && frameStart < time && time < frameEnd ? null : CurrentTime;
return CurrentTime = time;
// if not all frames are received but we are before the first frame, allow playing.
if (time < Frames[0].Time)
return CurrentTime = time;
// in the case we have no next frames and haven't received enough frame data, block.
return null;
} }
private void updateDirection(double time) private double getFrameTime(int index)
{ {
if (!CurrentTime.HasValue) if (index < 0)
{ return double.NegativeInfinity;
currentDirection = 1; if (index >= Frames.Count)
} return double.PositiveInfinity;
else
{ return Frames[index].Time;
currentDirection = time.CompareTo(CurrentTime);
if (currentDirection == 0) currentDirection = 1;
}
} }
} }
} }

View File

@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
public int JudgedHits { get; private set; } public int JudgedHits { get; private set; }
private JudgementResult lastAppliedResult;
private readonly BindableBool hasCompleted = new BindableBool(); private readonly BindableBool hasCompleted = new BindableBool();
/// <summary> /// <summary>
@ -53,12 +55,11 @@ namespace osu.Game.Rulesets.Scoring
public void ApplyResult(JudgementResult result) public void ApplyResult(JudgementResult result)
{ {
JudgedHits++; JudgedHits++;
lastAppliedResult = result;
ApplyResultInternal(result); ApplyResultInternal(result);
NewJudgement?.Invoke(result); NewJudgement?.Invoke(result);
updateHasCompleted();
} }
/// <summary> /// <summary>
@ -69,8 +70,6 @@ namespace osu.Game.Rulesets.Scoring
{ {
JudgedHits--; JudgedHits--;
updateHasCompleted();
RevertResultInternal(result); RevertResultInternal(result);
} }
@ -134,6 +133,10 @@ namespace osu.Game.Rulesets.Scoring
} }
} }
private void updateHasCompleted() => hasCompleted.Value = JudgedHits == MaxHits; protected override void Update()
{
base.Update();
hasCompleted.Value = JudgedHits == MaxHits && (JudgedHits == 0 || lastAppliedResult.TimeAbsolute < Clock.CurrentTime);
}
} }
} }

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.UI
var enumerable = HitObjectContainer.Objects; var enumerable = HitObjectContainer.Objects;
if (nestedPlayfields.IsValueCreated) if (nestedPlayfields.Count != 0)
enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)); enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects));
return enumerable; return enumerable;
@ -76,9 +76,9 @@ namespace osu.Game.Rulesets.UI
/// <summary> /// <summary>
/// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>. /// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>.
/// </summary> /// </summary>
public IEnumerable<Playfield> NestedPlayfields => nestedPlayfields.IsValueCreated ? nestedPlayfields.Value : Enumerable.Empty<Playfield>(); public IEnumerable<Playfield> NestedPlayfields => nestedPlayfields;
private readonly Lazy<List<Playfield>> nestedPlayfields = new Lazy<List<Playfield>>(); private readonly List<Playfield> nestedPlayfields = new List<Playfield>();
/// <summary> /// <summary>
/// Whether judgements should be displayed by this and and all nested <see cref="Playfield"/>s. /// Whether judgements should be displayed by this and and all nested <see cref="Playfield"/>s.
@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.UI
otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h);
otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
nestedPlayfields.Value.Add(otherPlayfield); nestedPlayfields.Add(otherPlayfield);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -279,12 +279,7 @@ namespace osu.Game.Rulesets.UI
return true; return true;
} }
bool removedFromNested = false; return nestedPlayfields.Any(p => p.Remove(hitObject));
if (nestedPlayfields.IsValueCreated)
removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(hitObject));
return removedFromNested;
} }
/// <summary> /// <summary>
@ -429,10 +424,7 @@ namespace osu.Game.Rulesets.UI
return; return;
} }
if (!nestedPlayfields.IsValueCreated) foreach (var p in nestedPlayfields)
return;
foreach (var p in nestedPlayfields.Value)
p.SetKeepAlive(hitObject, keepAlive); p.SetKeepAlive(hitObject, keepAlive);
} }
@ -444,10 +436,7 @@ namespace osu.Game.Rulesets.UI
foreach (var (_, entry) in lifetimeEntryMap) foreach (var (_, entry) in lifetimeEntryMap)
entry.KeepAlive = true; entry.KeepAlive = true;
if (!nestedPlayfields.IsValueCreated) foreach (var p in nestedPlayfields)
return;
foreach (var p in nestedPlayfields.Value)
p.KeepAllAlive(); p.KeepAllAlive();
} }
@ -461,10 +450,7 @@ namespace osu.Game.Rulesets.UI
{ {
HitObjectContainer.PastLifetimeExtension = value; HitObjectContainer.PastLifetimeExtension = value;
if (!nestedPlayfields.IsValueCreated) foreach (var nested in nestedPlayfields)
return;
foreach (var nested in nestedPlayfields.Value)
nested.PastLifetimeExtension = value; nested.PastLifetimeExtension = value;
} }
} }
@ -479,10 +465,7 @@ namespace osu.Game.Rulesets.UI
{ {
HitObjectContainer.FutureLifetimeExtension = value; HitObjectContainer.FutureLifetimeExtension = value;
if (!nestedPlayfields.IsValueCreated) foreach (var nested in nestedPlayfields)
return;
foreach (var nested in nestedPlayfields.Value)
nested.FutureLifetimeExtension = value; nested.FutureLifetimeExtension = value;
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -10,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.StateChanges.Events;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -100,6 +102,17 @@ namespace osu.Game.Rulesets.UI
#endregion #endregion
// to avoid allocation
private readonly List<IInput> emptyInputList = new List<IInput>();
protected override List<IInput> GetPendingInputs()
{
if (replayInputHandler?.IsActive == false)
return emptyInputList;
return base.GetPendingInputs();
}
#region Setting application (disables etc.) #region Setting application (disables etc.)
private Bindable<bool> mouseDisabled; private Bindable<bool> mouseDisabled;

View File

@ -27,9 +27,12 @@ namespace osu.Game.Screens.Backgrounds
private WorkingBeatmap beatmap; private WorkingBeatmap beatmap;
/// <summary> /// <summary>
/// Whether or not user dim settings should be applied to this Background. /// Whether or not user-configured settings relating to brightness of elements should be ignored.
/// </summary> /// </summary>
public readonly Bindable<bool> EnableUserDim = new Bindable<bool>(); /// <remarks>
/// Beatmap background screens should not apply user settings by default.
/// </remarks>
public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>(true);
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>(); public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
@ -50,7 +53,7 @@ namespace osu.Game.Screens.Backgrounds
InternalChild = dimmable = CreateFadeContainer(); InternalChild = dimmable = CreateFadeContainer();
dimmable.EnableUserDim.BindTo(EnableUserDim); dimmable.IgnoreUserSettings.BindTo(IgnoreUserSettings);
dimmable.IsBreakTime.BindTo(IsBreakTime); dimmable.IsBreakTime.BindTo(IsBreakTime);
dimmable.BlurAmount.BindTo(BlurAmount); dimmable.BlurAmount.BindTo(BlurAmount);
@ -148,7 +151,7 @@ namespace osu.Game.Screens.Backgrounds
/// <summary> /// <summary>
/// As an optimisation, we add the two blur portions to be applied rather than actually applying two separate blurs. /// As an optimisation, we add the two blur portions to be applied rather than actually applying two separate blurs.
/// </summary> /// </summary>
private Vector2 blurTarget => EnableUserDim.Value private Vector2 blurTarget => !IgnoreUserSettings.Value
? new Vector2(BlurAmount.Value + (float)userBlurLevel.Value * USER_BLUR_FACTOR) ? new Vector2(BlurAmount.Value + (float)userBlurLevel.Value * USER_BLUR_FACTOR)
: new Vector2(BlurAmount.Value); : new Vector2(BlurAmount.Value);
@ -166,7 +169,9 @@ namespace osu.Game.Screens.Backgrounds
BlurAmount.ValueChanged += _ => UpdateVisuals(); BlurAmount.ValueChanged += _ => UpdateVisuals();
} }
protected override bool ShowDimContent => !ShowStoryboard.Value || !StoryboardReplacesBackground.Value; // The background needs to be hidden in the case of it being replaced by the storyboard protected override bool ShowDimContent
// The background needs to be hidden in the case of it being replaced by the storyboard
=> (!ShowStoryboard.Value && !IgnoreUserSettings.Value) || !StoryboardReplacesBackground.Value;
protected override void UpdateVisuals() protected override void UpdateVisuals()
{ {

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) => Colour = colours.Yellow; private void load(OsuColour colours) => Colour = colours.GreyCarmineLight;
} }
} }
} }

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
public class ControlPointVisualisation : PointVisualisation
{
protected readonly ControlPoint Point;
public ControlPointVisualisation(ControlPoint point)
{
Point = point;
Height = 0.25f;
Origin = Anchor.TopCentre;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = Point.GetRepresentingColour(colours);
}
}
}

View File

@ -0,0 +1,72 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{
public class EffectPointVisualisation : CompositeDrawable
{
private readonly EffectControlPoint effect;
private Bindable<bool> kiai;
[Resolved]
private EditorBeatmap beatmap { get; set; }
[Resolved]
private OsuColour colours { get; set; }
public EffectPointVisualisation(EffectControlPoint point)
{
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Y;
effect = point;
}
[BackgroundDependencyLoader]
private void load()
{
kiai = effect.KiaiModeBindable.GetBoundCopy();
kiai.BindValueChanged(_ =>
{
ClearInternal();
AddInternal(new ControlPointVisualisation(effect));
if (!kiai.Value)
return;
var endControlPoint = beatmap.ControlPointInfo.EffectPoints.FirstOrDefault(c => c.Time > effect.Time && !c.KiaiMode);
// handle kiai duration
// eventually this will be simpler when we have control points with durations.
if (endControlPoint != null)
{
RelativeSizeAxes = Axes.Both;
Origin = Anchor.TopLeft;
Width = (float)(endControlPoint.Time - effect.Time);
AddInternal(new PointVisualisation
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.TopLeft,
Width = 1,
Height = 0.25f,
Depth = float.MaxValue,
Colour = effect.GetRepresentingColour(colours).Darken(0.5f),
});
}
}, true);
}
}
}

View File

@ -1,29 +1,33 @@
// 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.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
{ {
public class GroupVisualisation : PointVisualisation public class GroupVisualisation : CompositeDrawable
{ {
[Resolved]
private OsuColour colours { get; set; }
public readonly ControlPointGroup Group; public readonly ControlPointGroup Group;
private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>(); private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
[Resolved]
private OsuColour colours { get; set; }
public GroupVisualisation(ControlPointGroup group) public GroupVisualisation(ControlPointGroup group)
: base(group.Time)
{ {
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both;
Origin = Anchor.TopLeft;
Group = group; Group = group;
X = (float)group.Time;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -33,13 +37,32 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
controlPoints.BindTo(Group.ControlPoints); controlPoints.BindTo(Group.ControlPoints);
controlPoints.BindCollectionChanged((_, __) => controlPoints.BindCollectionChanged((_, __) =>
{ {
if (controlPoints.Count == 0) ClearInternal();
{
Colour = Color4.Transparent;
return;
}
Colour = controlPoints.Any(c => c is TimingControlPoint) ? colours.YellowDark : colours.Green; if (controlPoints.Count == 0)
return;
foreach (var point in Group.ControlPoints)
{
switch (point)
{
case TimingControlPoint _:
AddInternal(new ControlPointVisualisation(point) { Y = 0, });
break;
case DifficultyControlPoint _:
AddInternal(new ControlPointVisualisation(point) { Y = 0.25f, });
break;
case SampleControlPoint _:
AddInternal(new ControlPointVisualisation(point) { Y = 0.5f, });
break;
case EffectControlPoint effect:
AddInternal(new EffectPointVisualisation(effect) { Y = 0.75f });
break;
}
}
}, true); }, true);
} }
} }

View File

@ -27,6 +27,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Y = -10,
Height = 0.35f Height = 0.35f
}, },
new BookmarkPart new BookmarkPart
@ -38,6 +39,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
}, },
new Container new Container
{ {
Name = "centre line",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colours.Gray5, Colour = colours.Gray5,
Children = new Drawable[] Children = new Drawable[]
@ -45,7 +47,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
new Circle new Circle
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight, Origin = Anchor.Centre,
Size = new Vector2(5) Size = new Vector2(5)
}, },
new Box new Box
@ -59,7 +61,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
new Circle new Circle
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft, Origin = Anchor.Centre,
Size = new Vector2(5) Size = new Vector2(5)
}, },
} }
@ -69,7 +71,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Height = 0.25f Height = 0.10f
} }
}; };
} }

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 osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
@ -10,19 +9,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
/// <summary> /// <summary>
/// Represents a spanning point on a timeline part. /// Represents a spanning point on a timeline part.
/// </summary> /// </summary>
public class DurationVisualisation : Container public class DurationVisualisation : Circle
{ {
protected DurationVisualisation(double startTime, double endTime) protected DurationVisualisation(double startTime, double endTime)
{ {
Masking = true;
CornerRadius = 5;
RelativePositionAxes = Axes.X; RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
X = (float)startTime; X = (float)startTime;
Width = (float)(endTime - startTime); Width = (float)(endTime - startTime);
AddInternal(new Box { RelativeSizeAxes = Axes.Both });
} }
} }
} }

View File

@ -9,7 +9,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
/// <summary> /// <summary>
/// Represents a singular point on a timeline part. /// Represents a singular point on a timeline part.
/// </summary> /// </summary>
public class PointVisualisation : Box public class PointVisualisation : Circle
{ {
public const float MAX_WIDTH = 4; public const float MAX_WIDTH = 4;
@ -21,9 +21,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
public PointVisualisation() public PointVisualisation()
{ {
Origin = Anchor.TopCentre; RelativePositionAxes = Axes.Both;
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Anchor = Anchor.CentreLeft; Anchor = Anchor.CentreLeft;

View File

@ -1,67 +1,27 @@
// 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.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class DifficultyPointPiece : CompositeDrawable public class DifficultyPointPiece : TopPointPiece
{ {
private readonly DifficultyControlPoint difficultyPoint;
private OsuSpriteText speedMultiplierText;
private readonly BindableNumber<double> speedMultiplier; private readonly BindableNumber<double> speedMultiplier;
public DifficultyPointPiece(DifficultyControlPoint difficultyPoint) public DifficultyPointPiece(DifficultyControlPoint point)
: base(point)
{ {
this.difficultyPoint = difficultyPoint; speedMultiplier = point.SpeedMultiplierBindable.GetBoundCopy();
speedMultiplier = difficultyPoint.SpeedMultiplierBindable.GetBoundCopy();
Y = Height;
} }
[BackgroundDependencyLoader] protected override void LoadComplete()
private void load(OsuColour colours)
{ {
RelativeSizeAxes = Axes.Y; base.LoadComplete();
AutoSizeAxes = Axes.X; speedMultiplier.BindValueChanged(multiplier => Label.Text = $"{multiplier.NewValue:n2}x", true);
Color4 colour = difficultyPoint.GetRepresentingColour(colours);
InternalChildren = new Drawable[]
{
new Box
{
Colour = colour,
Width = 2,
RelativeSizeAxes = Axes.Y,
},
new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = colour,
RelativeSizeAxes = Axes.Both,
},
speedMultiplierText = new OsuSpriteText
{
Font = OsuFont.Default.With(weight: FontWeight.Bold),
Colour = Color4.White,
}
}
},
};
speedMultiplier.BindValueChanged(multiplier => speedMultiplierText.Text = $"{multiplier.NewValue:n2}x", true);
} }
} }
} }

View File

@ -3,9 +3,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -23,7 +21,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly BindableNumber<int> volume; private readonly BindableNumber<int> volume;
private OsuSpriteText text; private OsuSpriteText text;
private Box volumeBox; private Container volumeBox;
private const int max_volume_height = 22;
public SamplePointPiece(SampleControlPoint samplePoint) public SamplePointPiece(SampleControlPoint samplePoint)
{ {
@ -35,8 +35,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
Origin = Anchor.TopLeft; Margin = new MarginPadding { Vertical = 5 };
Anchor = Anchor.TopLeft;
Origin = Anchor.BottomCentre;
Anchor = Anchor.BottomCentre;
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
@ -45,40 +47,43 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
volumeBox = new Circle
{
CornerRadius = 5,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Y = -20,
Width = 10,
Colour = colour,
},
new Container new Container
{ {
RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X,
Width = 20, Height = 16,
Masking = true,
CornerRadius = 8,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Children = new Drawable[] Children = new Drawable[]
{ {
volumeBox = new Box
{
X = 2,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Colour = ColourInfo.GradientVertical(colour, Color4.Black),
RelativeSizeAxes = Axes.Both,
},
new Box new Box
{ {
Colour = colour.Lighten(0.2f), Colour = colour,
Width = 2, RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.Y,
}, },
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(5),
Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
Colour = colours.B5,
}
} }
}, },
text = new OsuSpriteText
{
X = 2,
Y = -5,
Anchor = Anchor.BottomLeft,
Alpha = 0.9f,
Rotation = -90,
Font = OsuFont.Default.With(weight: FontWeight.SemiBold)
}
}; };
volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true); volume.BindValueChanged(volume => volumeBox.Height = max_volume_height * volume.NewValue / 100f, true);
bank.BindValueChanged(bank => text.Text = bank.NewValue, true); bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
} }
} }

View File

@ -23,6 +23,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Cached] [Cached]
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
{ {
private readonly Drawable userContent;
public readonly Bindable<bool> WaveformVisible = new Bindable<bool>(); public readonly Bindable<bool> WaveformVisible = new Bindable<bool>();
public readonly Bindable<bool> ControlPointsVisible = new Bindable<bool>(); public readonly Bindable<bool> ControlPointsVisible = new Bindable<bool>();
@ -56,8 +57,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Track track; private Track track;
public Timeline() private const float timeline_height = 72;
private const float timeline_expanded_height = 156;
public Timeline(Drawable userContent)
{ {
this.userContent = userContent;
RelativeSizeAxes = Axes.X;
ZoomDuration = 200; ZoomDuration = 200;
ZoomEasing = Easing.OutQuint; ZoomEasing = Easing.OutQuint;
ScrollbarVisible = false; ScrollbarVisible = false;
@ -69,18 +77,31 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private TimelineControlPointDisplay controlPoints; private TimelineControlPointDisplay controlPoints;
private Container mainContent;
private Bindable<float> waveformOpacity; private Bindable<float> waveformOpacity;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config) private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config)
{ {
CentreMarker centreMarker;
// We don't want the centre marker to scroll
AddInternal(centreMarker = new CentreMarker());
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
new Container controlPoints = new TimelineControlPointDisplay
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.X,
Height = timeline_expanded_height,
},
mainContent = new Container
{
RelativeSizeAxes = Axes.X,
Height = timeline_height,
Depth = float.MaxValue, Depth = float.MaxValue,
Children = new Drawable[] Children = new[]
{ {
waveform = new WaveformGraph waveform = new WaveformGraph
{ {
@ -90,8 +111,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
MidColour = colours.BlueDark, MidColour = colours.BlueDark,
HighColour = colours.BlueDarker, HighColour = colours.BlueDarker,
}, },
centreMarker.CreateProxy(),
ticks = new TimelineTickDisplay(), ticks = new TimelineTickDisplay(),
controlPoints = new TimelineControlPointDisplay(),
new Box new Box
{ {
Name = "zero marker", Name = "zero marker",
@ -100,21 +121,43 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Colour = colours.YellowDarker, Colour = colours.YellowDarker,
}, },
userContent,
} }
}, },
}); });
// We don't want the centre marker to scroll
AddInternal(new CentreMarker { Depth = float.MaxValue });
waveformOpacity = config.GetBindable<float>(OsuSetting.EditorWaveformOpacity); waveformOpacity = config.GetBindable<float>(OsuSetting.EditorWaveformOpacity);
Beatmap.BindTo(beatmap);
}
protected override void LoadComplete()
{
base.LoadComplete();
waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true); waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true);
WaveformVisible.ValueChanged += _ => updateWaveformOpacity(); WaveformVisible.ValueChanged += _ => updateWaveformOpacity();
ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
ControlPointsVisible.BindValueChanged(visible =>
{
if (visible.NewValue)
{
this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint);
mainContent.MoveToY(36, 200, Easing.OutQuint);
// delay the fade in else masking looks weird.
controlPoints.Delay(180).FadeIn(400, Easing.OutQuint);
}
else
{
controlPoints.FadeOut(200, Easing.OutQuint);
// likewise, delay the resize until the fade is complete.
this.Delay(180).ResizeHeightTo(timeline_height, 200, Easing.OutQuint);
mainContent.Delay(180).MoveToY(0, 200, Easing.OutQuint);
}
}, true);
Beatmap.BindTo(beatmap);
Beatmap.BindValueChanged(b => Beatmap.BindValueChanged(b =>
{ {
waveform.Waveform = b.NewValue.Waveform; waveform.Waveform = b.NewValue.Waveform;

View File

@ -12,11 +12,19 @@ using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class TimelineArea : Container public class TimelineArea : CompositeDrawable
{ {
public readonly Timeline Timeline = new Timeline { RelativeSizeAxes = Axes.Both }; public Timeline Timeline;
protected override Container<Drawable> Content => Timeline; private readonly Drawable userContent;
public TimelineArea(Drawable content = null)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
userContent = content ?? Drawable.Empty();
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -37,7 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}, },
new GridContainer new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] Content = new[]
{ {
new Drawable[] new Drawable[]
@ -55,11 +64,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}, },
new FillFlowContainer new FillFlowContainer
{ {
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Width = 160, Width = 160,
Padding = new MarginPadding { Horizontal = 10 }, Padding = new MarginPadding(10),
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4), Spacing = new Vector2(0, 4),
Children = new[] Children = new[]
@ -123,14 +130,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
} }
}, },
Timeline Timeline = new Timeline(userContent),
}, },
}, },
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
},
ColumnDimensions = new[] ColumnDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Distributed), new Dimension(),
} }
} }
}; };

View File

@ -65,6 +65,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
Colour = Color4.Black, Colour = Color4.Black,
Depth = float.MaxValue, Depth = float.MaxValue,
Blending = BlendingParameters.Additive,
}); });
} }

View File

@ -4,7 +4,6 @@
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@ -17,11 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>(); private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
public TimelineControlPointDisplay()
{
RelativeSizeAxes = Axes.Both;
}
protected override void LoadBeatmap(EditorBeatmap beatmap) protected override void LoadBeatmap(EditorBeatmap beatmap)
{ {
base.LoadBeatmap(beatmap); base.LoadBeatmap(beatmap);

View File

@ -27,6 +27,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
Origin = Anchor.TopCentre;
X = (float)group.Time; X = (float)group.Time;
} }

View File

@ -345,7 +345,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary> /// </summary>
public class ExtendableCircle : CompositeDrawable public class ExtendableCircle : CompositeDrawable
{ {
private readonly CircularContainer content; private readonly Circle content;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => content.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => content.ReceivePositionalInputAt(screenSpacePos);
@ -354,19 +354,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public ExtendableCircle() public ExtendableCircle()
{ {
Padding = new MarginPadding { Horizontal = -circle_size / 2f }; Padding = new MarginPadding { Horizontal = -circle_size / 2f };
InternalChild = content = new CircularContainer InternalChild = content = new Circle
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Shadow, Type = EdgeEffectType.Shadow,
Radius = 5, Radius = 5,
Colour = Color4.Black.Opacity(0.4f) Colour = Color4.Black.Opacity(0.4f)
},
Child = new Box
{
RelativeSizeAxes = Axes.Both
} }
}; };
} }

View File

@ -3,60 +3,27 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class TimingPointPiece : CompositeDrawable public class TimingPointPiece : TopPointPiece
{ {
private readonly TimingControlPoint point;
private readonly BindableNumber<double> beatLength; private readonly BindableNumber<double> beatLength;
private OsuSpriteText bpmText;
public TimingPointPiece(TimingControlPoint point) public TimingPointPiece(TimingControlPoint point)
: base(point)
{ {
this.point = point;
beatLength = point.BeatLengthBindable.GetBoundCopy(); beatLength = point.BeatLengthBindable.GetBoundCopy();
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
Origin = Anchor.CentreLeft;
Anchor = Anchor.CentreLeft;
AutoSizeAxes = Axes.Both;
Color4 colour = point.GetRepresentingColour(colours);
InternalChildren = new Drawable[]
{
new Box
{
Alpha = 0.9f,
Colour = ColourInfo.GradientHorizontal(colour, colour.Opacity(0.5f)),
RelativeSizeAxes = Axes.Both,
},
bpmText = new OsuSpriteText
{
Alpha = 0.9f,
Padding = new MarginPadding(3),
Font = OsuFont.Default.With(size: 40)
}
};
beatLength.BindValueChanged(beatLength => beatLength.BindValueChanged(beatLength =>
{ {
bpmText.Text = $"{60000 / beatLength.NewValue:n1} BPM"; Label.Text = $"{60000 / beatLength.NewValue:n1} BPM";
}, true); }, true);
} }
} }

View File

@ -0,0 +1,55 @@
// 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.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TopPointPiece : CompositeDrawable
{
private readonly ControlPoint point;
protected OsuSpriteText Label { get; private set; }
public TopPointPiece(ControlPoint point)
{
this.point = point;
AutoSizeAxes = Axes.X;
Height = 16;
Margin = new MarginPadding(4);
Masking = true;
CornerRadius = Height / 2;
Origin = Anchor.TopCentre;
Anchor = Anchor.TopCentre;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
new Box
{
Colour = point.GetRepresentingColour(colours),
RelativeSizeAxes = Axes.Both,
},
Label = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(3),
Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold),
Colour = colours.B5,
}
};
}
}
}

View File

@ -467,7 +467,7 @@ namespace osu.Game.Screens.Edit
// todo: temporary. we want to be applying dim using the UserDimContainer eventually. // todo: temporary. we want to be applying dim using the UserDimContainer eventually.
b.FadeColour(Color4.DarkGray, 500); b.FadeColour(Color4.DarkGray, 500);
b.EnableUserDim.Value = false; b.IgnoreUserSettings.Value = true;
b.BlurAmount.Value = 0; b.BlurAmount.Value = 0;
}); });

View File

@ -19,8 +19,6 @@ namespace osu.Game.Screens.Edit
private const float vertical_margins = 10; private const float vertical_margins = 10;
private const float horizontal_margins = 20; private const float horizontal_margins = 20;
private const float timeline_height = 110;
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
private Container timelineContainer; private Container timelineContainer;
@ -40,64 +38,87 @@ namespace osu.Game.Screens.Edit
if (beatDivisor != null) if (beatDivisor != null)
this.beatDivisor.BindTo(beatDivisor); this.beatDivisor.BindTo(beatDivisor);
Children = new Drawable[] Child = new GridContainer
{ {
mainContent = new Container RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{ {
Name = "Main content", new Dimension(GridSizeMode.AutoSize),
RelativeSizeAxes = Axes.Both, new Dimension(),
Padding = new MarginPadding
{
Horizontal = horizontal_margins,
Top = vertical_margins + timeline_height,
Bottom = vertical_margins
},
Child = spinner = new LoadingSpinner(true)
{
State = { Value = Visibility.Visible },
},
}, },
new Container Content = new[]
{ {
Name = "Timeline", new Drawable[]
RelativeSizeAxes = Axes.X,
Height = timeline_height,
Children = new Drawable[]
{ {
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.5f)
},
new Container new Container
{ {
Name = "Timeline content", Name = "Timeline",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins }, AutoSizeAxes = Axes.Y,
Child = new GridContainer Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, new Box
Content = new[]
{ {
new Drawable[] RelativeSizeAxes = Axes.Both,
{ Colour = Color4.Black.Opacity(0.5f)
timelineContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 },
},
new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
},
}, },
ColumnDimensions = new[] new Container
{ {
new Dimension(), Name = "Timeline content",
new Dimension(GridSizeMode.Absolute, 90), RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins },
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
{
new Drawable[]
{
timelineContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 5 },
},
new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
},
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 90),
}
},
} }
}
},
},
new Drawable[]
{
mainContent = new Container
{
Name = "Main content",
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
Padding = new MarginPadding
{
Horizontal = horizontal_margins,
Top = vertical_margins,
Bottom = vertical_margins
}, },
} Child = spinner = new LoadingSpinner(true)
} {
}, State = { Value = Visibility.Visible },
},
},
},
}
}; };
} }
@ -112,14 +133,7 @@ namespace osu.Game.Screens.Edit
mainContent.Add(content); mainContent.Add(content);
content.FadeInFromZero(300, Easing.OutQuint); content.FadeInFromZero(300, Easing.OutQuint);
LoadComponentAsync(new TimelineArea LoadComponentAsync(new TimelineArea(CreateTimelineContent()), t =>
{
RelativeSizeAxes = Axes.Both,
Children = new[]
{
CreateTimelineContent(),
}
}, t =>
{ {
timelineContainer.Add(t); timelineContainer.Add(t);
OnTimelineLoaded(t); OnTimelineLoaded(t);

View File

@ -764,7 +764,7 @@ namespace osu.Game.Screens.Play
ApplyToBackground(b => ApplyToBackground(b =>
{ {
b.EnableUserDim.Value = true; b.IgnoreUserSettings.Value = false;
b.BlurAmount.Value = 0; b.BlurAmount.Value = 0;
// bind component bindables. // bind component bindables.
@ -913,7 +913,7 @@ namespace osu.Game.Screens.Play
float fadeOutDuration = instant ? 0 : 250; float fadeOutDuration = instant ? 0 : 250;
this.FadeOut(fadeOutDuration); this.FadeOut(fadeOutDuration);
ApplyToBackground(b => b.EnableUserDim.Value = false); ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
storyboardReplacesBackground.Value = false; storyboardReplacesBackground.Value = false;
} }

View File

@ -24,6 +24,7 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -112,6 +113,9 @@ namespace osu.Game.Screens.Play
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
[Resolved(CanBeNull = true)]
private BatteryInfo batteryInfo { get; set; }
public PlayerLoader(Func<Player> createPlayer) public PlayerLoader(Func<Player> createPlayer)
{ {
this.createPlayer = createPlayer; this.createPlayer = createPlayer;
@ -121,6 +125,7 @@ namespace osu.Game.Screens.Play
private void load(SessionStatics sessionStatics) private void load(SessionStatics sessionStatics)
{ {
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce); muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
InternalChild = (content = new LogoTrackingContainer InternalChild = (content = new LogoTrackingContainer
{ {
@ -196,6 +201,7 @@ namespace osu.Game.Screens.Play
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0)); Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0));
showMuteWarningIfNeeded(); showMuteWarningIfNeeded();
showBatteryWarningIfNeeded();
} }
public override void OnResuming(IScreen last) public override void OnResuming(IScreen last)
@ -229,7 +235,7 @@ namespace osu.Game.Screens.Play
content.ScaleTo(0.7f, 150, Easing.InQuint); content.ScaleTo(0.7f, 150, Easing.InQuint);
this.FadeOut(150); this.FadeOut(150);
ApplyToBackground(b => b.EnableUserDim.Value = false); ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
BackgroundBrightnessReduction = false; BackgroundBrightnessReduction = false;
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
@ -277,7 +283,7 @@ namespace osu.Game.Screens.Play
// Preview user-defined background dim and blur when hovered on the visual settings panel. // Preview user-defined background dim and blur when hovered on the visual settings panel.
ApplyToBackground(b => ApplyToBackground(b =>
{ {
b.EnableUserDim.Value = true; b.IgnoreUserSettings.Value = false;
b.BlurAmount.Value = 0; b.BlurAmount.Value = 0;
}); });
@ -288,7 +294,7 @@ namespace osu.Game.Screens.Play
ApplyToBackground(b => ApplyToBackground(b =>
{ {
// Returns background dim and blur to the values specified by PlayerLoader. // Returns background dim and blur to the values specified by PlayerLoader.
b.EnableUserDim.Value = false; b.IgnoreUserSettings.Value = true;
b.BlurAmount.Value = BACKGROUND_BLUR; b.BlurAmount.Value = BACKGROUND_BLUR;
}); });
@ -470,5 +476,48 @@ namespace osu.Game.Screens.Play
} }
#endregion #endregion
#region Low battery warning
private Bindable<bool> batteryWarningShownOnce;
private void showBatteryWarningIfNeeded()
{
if (batteryInfo == null) return;
if (!batteryWarningShownOnce.Value)
{
if (!batteryInfo.IsCharging && batteryInfo.ChargeLevel <= 0.25)
{
notificationOverlay?.Post(new BatteryWarningNotification());
batteryWarningShownOnce.Value = true;
}
}
}
private class BatteryWarningNotification : SimpleNotification
{
public override bool IsImportant => true;
public BatteryWarningNotification()
{
Text = "Your battery level is low! Charge your device to prevent interruptions during gameplay.";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, NotificationOverlay notificationOverlay)
{
Icon = FontAwesome.Solid.BatteryQuarter;
IconBackgound.Colour = colours.RedDark;
Activated = delegate
{
notificationOverlay.Hide();
return true;
};
}
}
#endregion
} }
} }

View File

@ -0,0 +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.
namespace osu.Game.Utils
{
/// <summary>
/// Provides access to the system's power status.
/// </summary>
public abstract class BatteryInfo
{
/// <summary>
/// The charge level of the battery, from 0 to 1.
/// </summary>
public abstract double ChargeLevel { get; }
public abstract bool IsCharging { get; }
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" /> <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.413.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.415.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
<PackageReference Include="Sentry" Version="3.2.0" /> <PackageReference Include="Sentry" Version="3.2.0" />
<PackageReference Include="SharpCompress" Version="0.28.1" /> <PackageReference Include="SharpCompress" Version="0.28.1" />

View File

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

View File

@ -5,6 +5,8 @@ using System;
using Foundation; using Foundation;
using osu.Game; using osu.Game;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils;
using Xamarin.Essentials;
namespace osu.iOS namespace osu.iOS
{ {
@ -13,5 +15,14 @@ namespace osu.iOS
public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString());
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();
private class IOSBatteryInfo : BatteryInfo
{
public override double ChargeLevel => Battery.ChargeLevel;
public override bool IsCharging => Battery.PowerSource != BatteryPowerSource.Battery;
}
} }
} }

View File

@ -116,5 +116,8 @@
<Visible>false</Visible> <Visible>false</Visible>
</ImageAsset> </ImageAsset>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" /> <Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
</Project> </Project>