1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 11:20:04 +08:00

Merge branch 'master' into refactor-framed-replay-input-hander

This commit is contained in:
Dan Balasescu 2021-04-15 21:48:58 +09:00 committed by GitHub
commit ac8e462d32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 3393 additions and 603 deletions

View File

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

View File

@ -7,6 +7,8 @@ using Android.OS;
using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
using osu.Game.Utils;
using Xamarin.Essentials;
namespace osu.Android
{
@ -72,5 +74,14 @@ namespace osu.Android
}
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.READ_FRAME_BUFFER" />
<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" />
</manifest>

View File

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

View File

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

View File

@ -0,0 +1,260 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
{
[TestFixture]
public class CheckOffscreenObjectsTest
{
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE * 0.5f;
private CheckOffscreenObjects check;
[SetUp]
public void Setup()
{
check = new CheckOffscreenObjects();
}
[Test]
public void TestCircleInCenter()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 3000,
Position = playfield_centre // Playfield is 640 x 480.
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestCircleNearEdge()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 3000,
Position = new Vector2(5, 5)
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestCircleNearEdgeStackedOffscreen()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 3000,
Position = new Vector2(5, 5),
StackHeight = 5
}
}
};
assertOffscreenCircle(beatmap);
}
[Test]
public void TestCircleOffscreen()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 3000,
Position = new Vector2(0, 0)
}
}
};
assertOffscreenCircle(beatmap);
}
[Test]
public void TestSliderInCenter()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = new Vector2(420, 240),
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(-100, 0))
}),
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestSliderNearEdge()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
}),
}
}
};
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestSliderNearEdgeStackedOffscreen()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
}),
StackHeight = 5
}
}
};
assertOffscreenSlider(beatmap);
}
[Test]
public void TestSliderOffscreenStart()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = new Vector2(0, 0),
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(playfield_centre)
}),
}
}
};
assertOffscreenSlider(beatmap);
}
[Test]
public void TestSliderOffscreenEnd()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = playfield_centre,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
new PathControlPoint(-playfield_centre)
}),
}
}
};
assertOffscreenSlider(beatmap);
}
[Test]
public void TestSliderOffscreenPath()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 3000,
Position = playfield_centre,
Path = new SliderPath(new[]
{
// Circular arc shoots over the top of the screen.
new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(-100, -200)),
new PathControlPoint(new Vector2(100, -200))
}),
}
}
};
assertOffscreenSlider(beatmap);
}
private void assertOffscreenCircle(IBeatmap beatmap)
{
var issues = check.Run(beatmap).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
}
private void assertOffscreenSlider(IBeatmap beatmap)
{
var issues = check.Run(beatmap).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider);
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneOsuEditorSelectInvalidPath : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestSelectDoesNotModify()
{
Slider slider = new Slider { StartTime = 0, Position = new Vector2(320, 40) };
PathControlPoint[] points =
{
new PathControlPoint(new Vector2(0), PathType.PerfectCurve),
new PathControlPoint(new Vector2(-100, 0)),
new PathControlPoint(new Vector2(100, 20))
};
int preSelectVersion = -1;
AddStep("add slider", () =>
{
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
preSelectVersion = slider.Path.Version.Value;
});
AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddAssert("slider same path", () => slider.Path.Version.Value == preSelectVersion);
}
}
}

View File

@ -59,11 +59,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
this.slider = slider;
ControlPoint = controlPoint;
// we don't want to run the path type update on construction as it may inadvertently change the slider.
cachePoints(slider);
slider.Path.Version.BindValueChanged(_ =>
{
PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
cachePoints(slider);
updatePathType();
}, runOnceImmediately: true);
});
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
@ -205,6 +208,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
/// <summary>
/// Handles correction of invalid path types.
/// </summary>

View File

@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckOffscreenObjects : ICheck
{
// A close approximation for the bounding box of the screen in gameplay on 4:3 aspect ratio.
// Uses gameplay space coordinates (512 x 384 playfield / 640 x 480 screen area).
// See https://github.com/ppy/osu/pull/12361#discussion_r612199777 for reference.
private const int min_x = -67;
private const int min_y = -60;
private const int max_x = 579;
private const int max_y = 428;
// The amount of milliseconds to step through a slider path at a time
// (higher = more performant, but higher false-negative chance).
private const int path_step_size = 5;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Offscreen hitobjects");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateOffscreenCircle(this),
new IssueTemplateOffscreenSlider(this)
};
public IEnumerable<Issue> Run(IBeatmap beatmap)
{
foreach (var hitobject in beatmap.HitObjects)
{
switch (hitobject)
{
case Slider slider:
{
foreach (var issue in sliderIssues(slider))
yield return issue;
break;
}
case HitCircle circle:
{
if (isOffscreen(circle.StackedPosition, circle.Radius))
yield return new IssueTemplateOffscreenCircle(this).Create(circle);
break;
}
}
}
}
/// <summary>
/// Steps through points on the slider to ensure the entire path is on-screen.
/// Returns at most one issue.
/// </summary>
/// <param name="slider">The slider whose path to check.</param>
/// <returns></returns>
private IEnumerable<Issue> sliderIssues(Slider slider)
{
for (int i = 0; i < slider.Distance; i += path_step_size)
{
double progress = i / slider.Distance;
Vector2 position = slider.StackedPositionAt(progress);
if (!isOffscreen(position, slider.Radius))
continue;
// `SpanDuration` ensures we don't include reverses.
double time = slider.StartTime + progress * slider.SpanDuration;
yield return new IssueTemplateOffscreenSlider(this).Create(slider, time);
yield break;
}
// Above loop may skip the last position in the slider due to step size.
if (!isOffscreen(slider.StackedEndPosition, slider.Radius))
yield break;
yield return new IssueTemplateOffscreenSlider(this).Create(slider, slider.EndTime);
}
private bool isOffscreen(Vector2 position, double radius)
{
return position.X - radius < min_x || position.X + radius > max_x ||
position.Y - radius < min_y || position.Y + radius > max_y;
}
public class IssueTemplateOffscreenCircle : IssueTemplate
{
public IssueTemplateOffscreenCircle(ICheck check)
: base(check, IssueType.Problem, "This circle goes offscreen on a 4:3 aspect ratio.")
{
}
public Issue Create(HitCircle circle) => new Issue(circle, this);
}
public class IssueTemplateOffscreenSlider : IssueTemplate
{
public IssueTemplateOffscreenSlider(ICheck check)
: base(check, IssueType.Problem, "This slider goes offscreen here on a 4:3 aspect ratio.")
{
}
public Issue Create(Slider slider, double offscreenTime) => new Issue(slider, this) { Time = offscreenTime };
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Edit.Checks;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuBeatmapVerifier : IBeatmapVerifier
{
private readonly List<ICheck> checks = new List<ICheck>
{
new CheckOffscreenObjects()
};
public IEnumerable<Issue> Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap));
}
}

View File

@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Name => "Touch Device";
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 ModType Type => ModType.System;

View File

@ -206,6 +206,8 @@ namespace osu.Game.Rulesets.Osu
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
public override IBeatmapVerifier CreateBeatmapVerifier() => new OsuBeatmapVerifier();
public override string Description => "osu!";
public override string ShortName => SHORT_NAME;

View File

@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckBackgroundTest
{
private CheckBackground check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckBackground();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo { Filename = "abc123.jpg" }
})
}
}
};
}
[Test]
public void TestBackgroundSetAndInFiles()
{
Assert.That(check.Run(beatmap), Is.Empty);
}
[Test]
public void TestBackgroundSetAndNotInFiles()
{
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
var issues = check.Run(beatmap).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateDoesNotExist);
}
[Test]
public void TestBackgroundNotSet()
{
beatmap.Metadata.BackgroundFile = null;
var issues = check.Run(beatmap).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateNoneSet);
}
}
}

View File

@ -0,0 +1,36 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Mods
{
[TestFixture]
public class ModSettingsEqualityComparison
{
[Test]
public void Test()
{
var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } };
var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
var apiMod1 = new APIMod(mod1);
var apiMod2 = new APIMod(mod2);
var apiMod3 = new APIMod(mod3);
Assert.That(mod1, Is.Not.EqualTo(mod2));
Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
Assert.That(mod2, Is.EqualTo(mod2));
Assert.That(apiMod2, Is.EqualTo(apiMod2));
Assert.That(mod2, Is.EqualTo(mod3));
Assert.That(apiMod2, Is.EqualTo(apiMod3));
Assert.That(mod3, Is.EqualTo(mod2));
Assert.That(apiMod3, Is.EqualTo(apiMod2));
}
}
}

View File

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

View File

@ -140,6 +140,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
@ -156,6 +157,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[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 Acronym => "TM";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
@ -116,6 +117,7 @@ namespace osu.Game.Tests.Online
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[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 Acronym => "TM";
public override string Description => "This is a test mod.";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]

View File

@ -65,6 +65,21 @@ namespace osu.Game.Tests.Visual.Background
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>
/// Check if <see cref="PlayerLoader"/> properly triggers the visual settings preview when a user hovers over the visual settings panel.
/// </summary>
@ -142,9 +157,9 @@ namespace osu.Game.Tests.Visual.Background
{
performFullSetup();
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());
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());
}
@ -161,13 +176,36 @@ namespace osu.Game.Tests.Visual.Background
player.ReplacesBackground.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);
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);
}
[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>
/// Check if the visual settings container retains dim and blur when pausing
/// </summary>
@ -204,17 +242,6 @@ namespace osu.Game.Tests.Visual.Background
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>
/// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim.
/// </summary>
@ -281,11 +308,11 @@ namespace osu.Game.Tests.Visual.Background
protected override BackgroundScreen CreateBackground()
{
background = new FadeAccessibleBackground(Beatmap.Value);
DimEnabled.BindTo(background.EnableUserDim);
IgnoreUserSettings.BindTo(background.IgnoreUserSettings);
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> BlurLevel = new BindableDouble();
@ -310,7 +337,7 @@ namespace osu.Game.Tests.Visual.Background
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;

View File

@ -1,88 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorQuickDelete : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First();
[Test]
public void TestQuickDeleteRemovesObject()
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("move mouse to object", () =>
{
var pos = blueprintContainer.ChildrenOfType<HitCirclePiece>().First().ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
}
[Test]
public void TestQuickDeleteRemovesSliderControlPoint()
{
Slider slider = new Slider { StartTime = 1000 };
PathControlPoint[] points =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(50, 0)),
new PathControlPoint(new Vector2(100, 0))
};
AddStep("add slider", () =>
{
slider.Path = new SliderPath(points);
EditorBeatmap.Add(slider);
});
AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to controlpoint", () =>
{
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2);
// second click should nuke the object completely.
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
}
}

View File

@ -0,0 +1,219 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorSelection : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
private BlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<BlueprintContainer>().First();
private void moveMouseToObject(Func<HitObject> targetFunc)
{
AddStep("move mouse to object", () =>
{
var pos = blueprintContainer.SelectionBlueprints
.First(s => s.HitObject == targetFunc())
.ChildrenOfType<HitCirclePiece>()
.First().ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
}
[Test]
public void TestBasicSelect()
{
var addedObject = new HitCircle { StartTime = 100 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
moveMouseToObject(() => addedObject);
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
var addedObject2 = new HitCircle
{
StartTime = 100,
Position = new Vector2(100),
};
AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2));
AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
moveMouseToObject(() => addedObject2);
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2);
}
[Test]
public void TestMultiSelect()
{
var addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) },
new HitCircle { StartTime = 300, Position = new Vector2(100) },
new HitCircle { StartTime = 400, Position = new Vector2(150) },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
moveMouseToObject(() => addedObjects[0]);
AddStep("click first", () => InputManager.Click(MouseButton.Left));
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]);
AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
moveMouseToObject(() => addedObjects[1]);
AddStep("click second", () => InputManager.Click(MouseButton.Left));
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
moveMouseToObject(() => addedObjects[2]);
AddStep("click third", () => InputManager.Click(MouseButton.Left));
AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2]));
moveMouseToObject(() => addedObjects[1]);
AddStep("click second", () => InputManager.Click(MouseButton.Left));
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
}
[TestCase(false)]
[TestCase(true)]
public void TestMultiSelectFromDrag(bool alreadySelectedBeforeDrag)
{
HitCircle[] addedObjects = null;
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) },
new HitCircle { StartTime = 300, Position = new Vector2(100) },
new HitCircle { StartTime = 400, Position = new Vector2(150) },
}));
moveMouseToObject(() => addedObjects[0]);
AddStep("click first", () => InputManager.Click(MouseButton.Left));
AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft));
moveMouseToObject(() => addedObjects[1]);
if (alreadySelectedBeforeDrag)
AddStep("click second", () => InputManager.Click(MouseButton.Left));
AddStep("mouse down on second", () => InputManager.PressButton(MouseButton.Left));
AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1]));
AddStep("drag to centre", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre));
AddAssert("positions changed", () => addedObjects[0].Position != Vector2.Zero && addedObjects[1].Position != new Vector2(50));
AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft));
AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left));
}
[Test]
public void TestBasicDeselect()
{
var addedObject = new HitCircle { StartTime = 100 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
moveMouseToObject(() => addedObject);
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject);
AddStep("click away", () =>
{
InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
});
AddAssert("selection lost", () => EditorBeatmap.SelectedHitObjects.Count == 0);
}
[Test]
public void TestQuickDeleteRemovesObject()
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
moveMouseToObject(() => addedObject);
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
}
[Test]
public void TestQuickDeleteRemovesSliderControlPoint()
{
Slider slider = null;
PathControlPoint[] points =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(50, 0)),
new PathControlPoint(new Vector2(100, 0))
};
AddStep("add slider", () =>
{
slider = new Slider
{
StartTime = 1000,
Path = new SliderPath(points)
};
EditorBeatmap.Add(slider);
});
AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("move mouse to controlpoint", () =>
{
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
});
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
AddStep("right click", () => InputManager.Click(MouseButton.Right));
AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2);
AddStep("right click", () => InputManager.Click(MouseButton.Right));
// second click should nuke the object completely.
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
}
}
}

View File

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

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestDisallowZeroDurationObjects()
{
DragBar dragBar;
DragArea dragArea;
AddStep("add spinner", () =>
{
@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Editing
EditorBeatmap.Add(new Spinner
{
Position = new Vector2(256, 256),
StartTime = 150,
StartTime = 2700,
Duration = 500
});
});
@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("hold down drag bar", () =>
{
// distinguishes between the actual drag bar and its "underlay shadow".
dragBar = this.ChildrenOfType<DragBar>().Single(bar => bar.HandlePositionalInput);
InputManager.MoveMouseTo(dragBar);
dragArea = this.ChildrenOfType<DragArea>().Single(bar => bar.HandlePositionalInput);
InputManager.MoveMouseTo(dragArea);
InputManager.PressButton(MouseButton.Left);
});

View File

@ -64,6 +64,13 @@ namespace osu.Game.Tests.Visual.Editing
});
}
protected override void LoadComplete()
{
base.LoadComplete();
Clock.Seek(2500);
}
public abstract Drawable CreateTestComponent();
private class AudioVisualiser : CompositeDrawable

View File

@ -25,6 +25,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@ -48,6 +49,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private readonly VolumeOverlay volumeOverlay;
[Cached(typeof(BatteryInfo))]
private readonly LocalBatteryInfo batteryInfo = new LocalBatteryInfo();
private readonly ChangelogOverlay changelogOverlay;
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]
public void TestEpilepsyWarningEarlyExit()
{
@ -321,6 +352,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public override string Name => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
public override string Description => string.Empty;
public bool Applied { get; private set; }
@ -348,5 +380,29 @@ namespace osu.Game.Tests.Visual.Gameplay
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

@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerSpectatorPlayerGrid : OsuManualInputManagerTestScene
{
private PlayerGrid grid;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = grid = new PlayerGrid { RelativeSizeAxes = Axes.Both };
});
[Test]
public void TestMaximiseAndMinimise()
{
addCells(2);
assertMaximisation(0, false, true);
assertMaximisation(1, false, true);
clickCell(0);
assertMaximisation(0, true);
assertMaximisation(1, false, true);
clickCell(0);
assertMaximisation(0, false);
assertMaximisation(1, false, true);
clickCell(1);
assertMaximisation(1, true);
assertMaximisation(0, false, true);
clickCell(1);
assertMaximisation(1, false);
assertMaximisation(0, false, true);
}
[Test]
public void TestClickBothCellsSimultaneously()
{
addCells(2);
AddStep("click cell 0 then 1", () =>
{
InputManager.MoveMouseTo(grid.Content.ElementAt(0));
InputManager.Click(MouseButton.Left);
InputManager.MoveMouseTo(grid.Content.ElementAt(1));
InputManager.Click(MouseButton.Left);
});
assertMaximisation(1, true);
assertMaximisation(0, false);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(9)]
[TestCase(11)]
[TestCase(12)]
[TestCase(15)]
[TestCase(16)]
public void TestCellCount(int count)
{
addCells(count);
AddWaitStep("wait for display", 2);
}
private void addCells(int count) => AddStep($"add {count} grid cells", () =>
{
for (int i = 0; i < count; i++)
grid.Add(new GridContent());
});
private void clickCell(int index) => AddStep($"click cell index {index}", () =>
{
InputManager.MoveMouseTo(grid.Content.ElementAt(index));
InputManager.Click(MouseButton.Left);
});
private void assertMaximisation(int index, bool shouldBeMaximised, bool instant = false)
{
string assertionText = $"cell index {index} {(shouldBeMaximised ? "is" : "is not")} maximised";
if (instant)
AddAssert(assertionText, checkAction);
else
AddUntilStep(assertionText, checkAction);
bool checkAction() => Precision.AlmostEquals(grid.MaximisedFacade.DrawSize, grid.Content.ElementAt(index).DrawSize, 10) == shouldBeMaximised;
}
private class GridContent : Box
{
public GridContent()
{
RelativeSizeAxes = Axes.Both;
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f);
}
}
}
}

View File

@ -19,13 +19,12 @@ namespace osu.Game.Tests.Visual.Online
{
UserHistoryGraph graph;
Add(graph = new UserHistoryGraph
Add(graph = new UserHistoryGraph("Test")
{
RelativeSizeAxes = Axes.X,
Height = 200,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
TooltipCounterName = "Test"
});
var values = new[]

View File

@ -57,6 +57,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private abstract class TestMod : Mod, IApplicableMod
{
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 string Description => "This is a customisable test mod.";
public override ModType Type => ModType.Conversion;
[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.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@ -3,7 +3,6 @@
using System;
using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
@ -143,7 +142,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
SetDefault(OsuSetting.EditorWaveformOpacity, 1f);
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
}
public OsuConfigManager(Storage storage)
@ -169,14 +168,9 @@ namespace osu.Game.Configuration
int combined = (year * 10000) + monthDay;
if (combined < 20200305)
if (combined < 20210413)
{
// the maximum value of this setting was changed.
// if we don't manually increase this, it causes song select to filter out beatmaps the user expects to see.
var maxStars = (BindableDouble)GetOriginalBindable<double>(OsuSetting.DisplayStarsMaximum);
if (maxStars.Value == 10)
maxStars.Value = maxStars.MaxValue;
SetValue(OsuSetting.EditorWaveformOpacity, 0.25f);
}
}

View File

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

View File

@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers
protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
protected override bool BlockScrollInput => false;
protected override bool BlockNonPositionalInput => true;
/// <summary>

View File

@ -23,11 +23,6 @@ namespace osu.Game.Graphics.Containers
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>
/// Whether or not user-configured settings relating to brightness of elements should be ignored
/// </summary>
@ -57,7 +52,7 @@ namespace osu.Game.Graphics.Containers
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;
@ -78,7 +73,6 @@ namespace osu.Game.Graphics.Containers
LightenDuringBreaks = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks);
ShowStoryboard = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
EnableUserDim.ValueChanged += _ => UpdateVisuals();
UserDimLevel.ValueChanged += _ => UpdateVisuals();
LightenDuringBreaks.ValueChanged += _ => UpdateVisuals();
IsBreakTime.ValueChanged += _ => UpdateVisuals();

View File

@ -70,6 +70,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode),
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
};
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
@ -97,9 +98,7 @@ namespace osu.Game.Input.Bindings
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
{
new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.IncreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume),
new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume),
new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute),
@ -249,5 +248,8 @@ namespace osu.Game.Input.Bindings
[Description("Beatmap Options")]
ToggleBeatmapOptions,
[Description("Verify mode")]
EditorVerifyMode,
}
}

View File

@ -0,0 +1,506 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using osu.Game.Database;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20210412045700_RefreshVolumeBindingsAgain")]
partial class RefreshVolumeBindingsAgain
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<float>("ApproachRate");
b.Property<float>("CircleSize");
b.Property<float>("DrainRate");
b.Property<float>("OverallDifficulty");
b.Property<double>("SliderMultiplier");
b.Property<double>("SliderTickRate");
b.HasKey("ID");
b.ToTable("BeatmapDifficulty");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("AudioLeadIn");
b.Property<double>("BPM");
b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor");
b.Property<int>("BeatmapSetInfoID");
b.Property<bool>("Countdown");
b.Property<double>("DistanceSpacing");
b.Property<int>("GridSize");
b.Property<string>("Hash");
b.Property<bool>("Hidden");
b.Property<double>("Length");
b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapID");
b.Property<string>("Path");
b.Property<int>("RulesetID");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");
b.Property<double>("StarDifficulty");
b.Property<int>("Status");
b.Property<string>("StoredBookmarks");
b.Property<double>("TimelineZoom");
b.Property<string>("Version");
b.Property<bool>("WidescreenStoryboard");
b.HasKey("ID");
b.HasIndex("BaseDifficultyID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("Hash");
b.HasIndex("MD5Hash");
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("BeatmapInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Artist");
b.Property<string>("ArtistUnicode");
b.Property<string>("AudioFile");
b.Property<string>("AuthorString")
.HasColumnName("Author");
b.Property<string>("BackgroundFile");
b.Property<int>("PreviewTime");
b.Property<string>("Source");
b.Property<string>("Tags");
b.Property<string>("Title");
b.Property<string>("TitleUnicode");
b.Property<string>("VideoFile");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.HasKey("ID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("FileInfoID");
b.ToTable("BeatmapSetFileInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset>("DateAdded");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapSetID");
b.Property<bool>("Protected");
b.Property<int>("Status");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapSetID")
.IsUnique();
b.ToTable("BeatmapSetInfo");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Key")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<int?>("SkinInfoID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("SkinInfoID");
b.HasIndex("RulesetID", "Variant");
b.ToTable("Settings");
});
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Hash");
b.Property<int>("ReferenceCount");
b.HasKey("ID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("ReferenceCount");
b.ToTable("FileInfo");
});
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntAction")
.HasColumnName("Action");
b.Property<string>("KeysString")
.HasColumnName("Keys");
b.Property<int?>("RulesetID");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("IntAction");
b.HasIndex("RulesetID", "Variant");
b.ToTable("KeyBinding");
});
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("Available");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.Property<string>("ShortName");
b.HasKey("ID");
b.HasIndex("Available");
b.HasIndex("ShortName")
.IsUnique();
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int?>("ScoreInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("ScoreInfoID");
b.ToTable("ScoreFileInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("Accuracy")
.HasColumnType("DECIMAL(1,4)");
b.Property<int>("BeatmapInfoID");
b.Property<int>("Combo");
b.Property<DateTimeOffset>("Date");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int>("MaxCombo");
b.Property<string>("ModsJson")
.HasColumnName("Mods");
b.Property<long?>("OnlineScoreID");
b.Property<double?>("PP");
b.Property<int>("Rank");
b.Property<int>("RulesetID");
b.Property<string>("StatisticsJson")
.HasColumnName("Statistics");
b.Property<long>("TotalScore");
b.Property<long?>("UserID")
.HasColumnName("UserID");
b.Property<string>("UserString")
.HasColumnName("User");
b.HasKey("ID");
b.HasIndex("BeatmapInfoID");
b.HasIndex("OnlineScoreID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("ScoreInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<string>("Name");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
.WithMany()
.HasForeignKey("BaseDifficultyID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
.WithMany("Beatmaps")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("Beatmaps")
.HasForeignKey("MetadataID");
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
.WithMany("Files")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Settings")
.HasForeignKey("SkinInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Scoring.ScoreInfo")
.WithMany("Files")
.HasForeignKey("ScoreInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
.WithMany("Scores")
.HasForeignKey("BeatmapInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,16 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
{
public partial class RefreshVolumeBindingsAgain : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DELETE FROM KeyBinding WHERE action in (6,7)");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -11,11 +11,12 @@ using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
namespace osu.Game.Online.API
{
[MessagePackObject]
public class APIMod : IMod
public class APIMod : IMod, IEquatable<APIMod>
{
[JsonProperty("acronym")]
[Key(0)]
@ -63,7 +64,16 @@ namespace osu.Game.Online.API
return resultMod;
}
public bool Equals(IMod other) => Acronym == other?.Acronym;
public bool Equals(IMod other) => other is APIMod them && Equals(them);
public bool Equals(APIMod other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Acronym == other.Acronym &&
Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
}
public override string ToString()
{
@ -72,5 +82,20 @@ namespace osu.Game.Online.API
return $"{Acronym}";
}
private class ModSettingsEqualityComparer : IEqualityComparer<KeyValuePair<string, object>>
{
public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer();
public bool Equals(KeyValuePair<string, object> x, KeyValuePair<string, object> y)
{
object xValue = ModUtils.GetSettingUnderlyingValue(x.Value);
object yValue = ModUtils.GetSettingUnderlyingValue(y.Value);
return x.Key == y.Key && EqualityComparer<object>.Default.Equals(xValue, yValue);
}
public int GetHashCode(KeyValuePair<string, object> obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value));
}
}
}

View File

@ -3,11 +3,10 @@
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using MessagePack;
using MessagePack.Formatters;
using osu.Framework.Bindables;
using osu.Game.Utils;
namespace osu.Game.Online.API
{
@ -24,36 +23,7 @@ namespace osu.Game.Online.API
var stringBytes = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(kvp.Key));
writer.WriteString(in stringBytes);
switch (kvp.Value)
{
case Bindable<double> d:
primitiveFormatter.Serialize(ref writer, d.Value, options);
break;
case Bindable<int> i:
primitiveFormatter.Serialize(ref writer, i.Value, options);
break;
case Bindable<float> f:
primitiveFormatter.Serialize(ref writer, f.Value, options);
break;
case Bindable<bool> b:
primitiveFormatter.Serialize(ref writer, b.Value, options);
break;
case IBindable u:
// A mod with unknown (e.g. enum) generic type.
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
Debug.Assert(valueMethod != null);
primitiveFormatter.Serialize(ref writer, valueMethod.GetValue(u), options);
break;
default:
// fall back for non-bindable cases.
primitiveFormatter.Serialize(ref writer, kvp.Value, options);
break;
}
primitiveFormatter.Serialize(ref writer, ModUtils.GetSettingUnderlyingValue(kvp.Value), options);
}
}

View File

@ -40,6 +40,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK.Input;
using RuntimeInfo = osu.Framework.RuntimeInfo;
@ -156,6 +157,8 @@ namespace osu.Game
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
protected virtual BatteryInfo CreateBatteryInfo() => null;
/// <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.
/// </summary>
@ -281,6 +284,11 @@ namespace osu.Game
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));
var powerStatus = CreateBatteryInfo();
if (powerStatus != null)
dependencies.CacheAs(powerStatus);
dependencies.Cache(new SessionStatics());
dependencies.Cache(new OsuColour());
@ -433,12 +441,15 @@ namespace osu.Game
if (paths.Length == 0)
return;
var extension = Path.GetExtension(paths.First())?.ToLowerInvariant();
var filesPerExtension = paths.GroupBy(p => Path.GetExtension(p).ToLowerInvariant());
foreach (var importer in fileImporters)
foreach (var groups in filesPerExtension)
{
if (importer.HandledExtensions.Contains(extension))
await importer.Import(paths).ConfigureAwait(false);
foreach (var importer in fileImporters)
{
if (importer.HandledExtensions.Contains(groups.Key))
await importer.Import(groups.ToArray()).ConfigureAwait(false);
}
}
}

View File

@ -15,6 +15,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
private ProfileLineChart chart;
/// <summary>
/// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the history graph tooltip.
/// </summary>
protected abstract string GraphCounterName { get; }
protected ChartProfileSubsection(Bindable<User> user, string headerText)
: base(user, headerText)
{
@ -30,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
Left = 20,
Right = 40
},
Child = chart = new ProfileLineChart()
Child = chart = new ProfileLineChart(GraphCounterName)
};
protected override void LoadComplete()

View File

@ -9,6 +9,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class PlayHistorySubsection : ChartProfileSubsection
{
protected override string GraphCounterName => "Plays";
public PlayHistorySubsection(Bindable<User> user)
: base(user, "Play History")
{

View File

@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
private readonly Container<TickLine> rowLinesContainer;
private readonly Container<TickLine> columnLinesContainer;
public ProfileLineChart()
public ProfileLineChart(string graphCounterName)
{
RelativeSizeAxes = Axes.X;
Height = 250;
@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
}
}
},
graph = new UserHistoryGraph
graph = new UserHistoryGraph(graphCounterName)
{
RelativeSizeAxes = Axes.Both
}

View File

@ -9,6 +9,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class ReplaysSubsection : ChartProfileSubsection
{
protected override string GraphCounterName => "Replays Watched";
public ReplaysSubsection(Bindable<User> user)
: base(user, "Replays Watched History")
{

View File

@ -11,25 +11,28 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
public class UserHistoryGraph : UserGraph<DateTime, long>
{
private readonly string tooltipCounterName;
[CanBeNull]
public UserHistoryCount[] Values
{
set => Data = value?.Select(v => new KeyValuePair<DateTime, long>(v.Date, v.Count)).ToArray();
}
/// <summary>
/// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the <see cref="HistoryGraphTooltip"/>.
/// </summary>
public string TooltipCounterName { get; set; } = "Plays";
public UserHistoryGraph(string tooltipCounterName)
{
this.tooltipCounterName = tooltipCounterName;
}
protected override float GetDataPointHeight(long playCount) => playCount;
protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(TooltipCounterName);
protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(tooltipCounterName);
protected override object GetTooltipContent(DateTime date, long playCount)
{
return new TooltipDisplayContent
{
Name = tooltipCounterName,
Count = playCount.ToString("N0"),
Date = date.ToString("MMMM yyyy")
};
@ -37,14 +40,17 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
protected class HistoryGraphTooltip : UserGraphTooltip
{
private readonly string tooltipCounterName;
public HistoryGraphTooltip(string tooltipCounterName)
: base(tooltipCounterName)
{
this.tooltipCounterName = tooltipCounterName;
}
public override bool SetContent(object content)
{
if (!(content is TooltipDisplayContent info))
if (!(content is TooltipDisplayContent info) || info.Name != tooltipCounterName)
return false;
Counter.Text = info.Count;
@ -55,6 +61,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
private class TooltipDisplayContent
{
public string Name;
public string Count;
public string Date;
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A ruleset-agnostic beatmap verifier that identifies issues in common metadata or mapping standards.
/// </summary>
public class BeatmapVerifier : IBeatmapVerifier
{
private readonly List<ICheck> checks = new List<ICheck>
{
new CheckBackground(),
};
public IEnumerable<Issue> Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap));
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckBackground : ICheck
{
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Resources, "Missing background");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateNoneSet(this),
new IssueTemplateDoesNotExist(this)
};
public IEnumerable<Issue> Run(IBeatmap beatmap)
{
if (beatmap.Metadata.BackgroundFile == null)
{
yield return new IssueTemplateNoneSet(this).Create();
yield break;
}
// If the background is set, also make sure it still exists.
var set = beatmap.BeatmapInfo.BeatmapSet;
var file = set.Files.FirstOrDefault(f => f.Filename == beatmap.Metadata.BackgroundFile);
if (file != null)
yield break;
yield return new IssueTemplateDoesNotExist(this).Create(beatmap.Metadata.BackgroundFile);
}
public class IssueTemplateNoneSet : IssueTemplate
{
public IssueTemplateNoneSet(ICheck check)
: base(check, IssueType.Problem, "No background has been set.")
{
}
public Issue Create() => new Issue(this);
}
public class IssueTemplateDoesNotExist : IssueTemplate
{
public IssueTemplateDoesNotExist(ICheck check)
: base(check, IssueType.Problem, "The background file \"{0}\" does not exist.")
{
}
public Issue Create(string filename) => new Issue(this, filename);
}
}
}

View File

@ -0,0 +1,61 @@
// 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.Rulesets.Edit.Checks.Components
{
/// <summary>
/// The category of an issue.
/// </summary>
public enum CheckCategory
{
/// <summary>
/// Anything to do with control points.
/// </summary>
Timing,
/// <summary>
/// Anything to do with artist, title, creator, etc.
/// </summary>
Metadata,
/// <summary>
/// Anything to do with non-audio files, e.g. background, skin, sprites, and video.
/// </summary>
Resources,
/// <summary>
/// Anything to do with audio files, e.g. song and hitsounds.
/// </summary>
Audio,
/// <summary>
/// Anything to do with files that don't fit into the above, e.g. unused, osu, or osb.
/// </summary>
Files,
/// <summary>
/// Anything to do with hitobjects unrelated to spread.
/// </summary>
Compose,
/// <summary>
/// Anything to do with difficulty levels or their progression.
/// </summary>
Spread,
/// <summary>
/// Anything to do with variables like CS, OD, AR, HP, and global SV.
/// </summary>
Settings,
/// <summary>
/// Anything to do with hitobject feedback.
/// </summary>
HitObjects,
/// <summary>
/// Anything to do with storyboarding, breaks, video offset, etc.
/// </summary>
Events
}
}

View File

@ -0,0 +1,24 @@
// 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.Rulesets.Edit.Checks.Components
{
public class CheckMetadata
{
/// <summary>
/// The category this check belongs to. E.g. <see cref="CheckCategory.Metadata"/>, <see cref="CheckCategory.Timing"/>, or <see cref="CheckCategory.Compose"/>.
/// </summary>
public readonly CheckCategory Category;
/// <summary>
/// Describes the issue(s) that this check looks for. Keep this brief, such that it fits into "No {description}". E.g. "Offscreen objects" / "Too short sliders".
/// </summary>
public readonly string Description;
public CheckMetadata(CheckCategory category, string description)
{
Category = category;
Description = description;
}
}
}

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 System.Collections.Generic;
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Edit.Checks.Components
{
/// <summary>
/// A specific check that can be run on a beatmap to verify or find issues.
/// </summary>
public interface ICheck
{
/// <summary>
/// The metadata for this check.
/// </summary>
public CheckMetadata Metadata { get; }
/// <summary>
/// All possible templates for issues that this check may return.
/// </summary>
public IEnumerable<IssueTemplate> PossibleTemplates { get; }
/// <summary>
/// Runs this check and returns any issues detected for the provided beatmap.
/// </summary>
/// <param name="beatmap">The beatmap to run the check on.</param>
public IEnumerable<Issue> Run(IBeatmap beatmap);
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Extensions;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Edit.Checks.Components
{
public class Issue
{
/// <summary>
/// The time which this issue is associated with, if any, otherwise null.
/// </summary>
public double? Time;
/// <summary>
/// The hitobjects which this issue is associated with. Empty by default.
/// </summary>
public IReadOnlyList<HitObject> HitObjects;
/// <summary>
/// The template which this issue is using. This provides properties such as the <see cref="IssueType"/>, and the <see cref="IssueTemplate.UnformattedMessage"/>.
/// </summary>
public IssueTemplate Template;
/// <summary>
/// The check that this issue originates from.
/// </summary>
public ICheck Check => Template.Check;
/// <summary>
/// The arguments that give this issue its context, based on the <see cref="IssueTemplate"/>. These are then substituted into the <see cref="IssueTemplate.UnformattedMessage"/>.
/// This could for instance include timestamps, which diff is being compared to, what some volume is, etc.
/// </summary>
public object[] Arguments;
public Issue(IssueTemplate template, params object[] args)
{
Time = null;
HitObjects = Array.Empty<HitObject>();
Template = template;
Arguments = args;
}
public Issue(double? time, IssueTemplate template, params object[] args)
: this(template, args)
{
Time = time;
}
public Issue(HitObject hitObject, IssueTemplate template, params object[] args)
: this(template, args)
{
Time = hitObject.StartTime;
HitObjects = new[] { hitObject };
}
public Issue(IEnumerable<HitObject> hitObjects, IssueTemplate template, params object[] args)
: this(template, args)
{
var hitObjectList = hitObjects.ToList();
Time = hitObjectList.FirstOrDefault()?.StartTime;
HitObjects = hitObjectList;
}
public override string ToString() => Template.GetMessage(Arguments);
public string GetEditorTimestamp()
{
return Time == null ? string.Empty : Time.Value.ToEditorFormattedString();
}
}
}

View File

@ -0,0 +1,74 @@
// 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 Humanizer;
using osu.Framework.Graphics;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Edit.Checks.Components
{
public class IssueTemplate
{
private static readonly Color4 problem_red = new Colour4(1.0f, 0.4f, 0.4f, 1.0f);
private static readonly Color4 warning_yellow = new Colour4(1.0f, 0.8f, 0.2f, 1.0f);
private static readonly Color4 negligible_green = new Colour4(0.33f, 0.8f, 0.5f, 1.0f);
private static readonly Color4 error_gray = new Colour4(0.5f, 0.5f, 0.5f, 1.0f);
/// <summary>
/// The check that this template originates from.
/// </summary>
public readonly ICheck Check;
/// <summary>
/// The type of the issue.
/// </summary>
public readonly IssueType Type;
/// <summary>
/// The unformatted message given when this issue is detected.
/// This gets populated later when an issue is constructed with this template.
/// E.g. "Inconsistent snapping (1/{0}) with [{1}] (1/{2})."
/// </summary>
public readonly string UnformattedMessage;
public IssueTemplate(ICheck check, IssueType type, string unformattedMessage)
{
Check = check;
Type = type;
UnformattedMessage = unformattedMessage;
}
/// <summary>
/// Returns the formatted message given the arguments used to format it.
/// </summary>
/// <param name="args">The arguments used to format the message.</param>
public string GetMessage(params object[] args) => UnformattedMessage.FormatWith(args);
/// <summary>
/// Returns the colour corresponding to the type of this issue.
/// </summary>
public Colour4 Colour
{
get
{
switch (Type)
{
case IssueType.Problem:
return problem_red;
case IssueType.Warning:
return warning_yellow;
case IssueType.Negligible:
return negligible_green;
case IssueType.Error:
return error_gray;
default:
return Color4.White;
}
}
}
}
}

View File

@ -0,0 +1,25 @@
// 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.Rulesets.Edit.Checks.Components
{
/// <summary>
/// The type, or severity, of an issue.
/// </summary>
public enum IssueType
{
/// <summary> A must-fix in the vast majority of cases. </summary>
Problem,
/// <summary> A possible mistake. Often requires critical thinking. </summary>
Warning,
// TODO: Try/catch all checks run and return error templates if exceptions occur.
/// <summary> An error occurred and a complete check could not be made. </summary>
Error,
// TODO: Negligible issues should be hidden by default.
/// <summary> A possible mistake so minor/unlikely that it can often be safely ignored. </summary>
Negligible,
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A class which can run against a beatmap and surface issues to the user which could go against known criteria or hinder gameplay.
/// </summary>
public interface IBeatmapVerifier
{
public IEnumerable<Issue> Run(IBeatmap beatmap);
}
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.UI;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mods
{
@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
/// The base class for gameplay modifiers.
/// </summary>
[ExcludeFromDynamicCompile]
public abstract class Mod : IMod, IJsonSerializable
public abstract class Mod : IMod, IEquatable<Mod>, IJsonSerializable
{
/// <summary>
/// The name of this mod.
@ -48,7 +49,7 @@ namespace osu.Game.Rulesets.Mods
/// The user readable description of this mod.
/// </summary>
[JsonIgnore]
public virtual string Description => string.Empty;
public abstract string Description { get; }
/// <summary>
/// The tooltip to display for this mod when used in a <see cref="ModIcon"/>.
@ -172,7 +173,19 @@ namespace osu.Game.Rulesets.Mods
target.Parse(source);
}
public bool Equals(IMod other) => GetType() == other?.GetType();
public bool Equals(IMod other) => other is Mod them && Equals(them);
public bool Equals(Mod other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return GetType() == other.GetType() &&
this.GetSettingsSourceProperties().All(pair =>
EqualityComparer<object>.Default.Equals(
ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(this)),
ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(other))));
}
/// <summary>
/// Reset all custom settings for this mod back to their defaults.

View File

@ -37,8 +37,7 @@ namespace osu.Game.Rulesets.Mods
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.BreakOverlay.Hide();

View File

@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Mods
{
public override string Name => "No Mod";
public override string Acronym => "NM";
public override string Description => "No mods applied.";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.Ban;
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;
private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<DrawableHitObject>();
private readonly List<DrawableHitObject> nestedHitObjects = new List<DrawableHitObject>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects;
/// <summary>
/// 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()!
drawableNested.ParentHitObject = this;
nestedHitObjects.Value.Add(drawableNested);
nestedHitObjects.Add(drawableNested);
AddNestedHitObject(drawableNested);
}
@ -305,19 +305,16 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (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.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
nestedHitObjects.Value.Clear();
ClearNestedHitObjects();
obj.OnNewResult -= onNewResult;
obj.OnRevertResult -= onRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
nestedHitObjects.Clear();
ClearNestedHitObjects();
HitObject.DefaultsApplied -= onDefaultsApplied;
OnFree();

View File

@ -201,6 +201,8 @@ namespace osu.Game.Rulesets
public virtual HitObjectComposer CreateHitObjectComposer() => null;
public virtual IBeatmapVerifier CreateBeatmapVerifier() => null;
public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle };
public virtual IResourceStore<byte[]> CreateResourceStore() => new NamespacedResourceStore<byte[]>(new DllResourceStore(GetType().Assembly), @"Resources");

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.UI
var enumerable = HitObjectContainer.Objects;
if (nestedPlayfields.IsValueCreated)
if (nestedPlayfields.Count != 0)
enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects));
return enumerable;
@ -76,9 +76,9 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>.
/// </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>
/// 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.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
nestedPlayfields.Value.Add(otherPlayfield);
nestedPlayfields.Add(otherPlayfield);
}
protected override void LoadComplete()
@ -279,12 +279,7 @@ namespace osu.Game.Rulesets.UI
return true;
}
bool removedFromNested = false;
if (nestedPlayfields.IsValueCreated)
removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(hitObject));
return removedFromNested;
return nestedPlayfields.Any(p => p.Remove(hitObject));
}
/// <summary>
@ -429,10 +424,7 @@ namespace osu.Game.Rulesets.UI
return;
}
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var p in nestedPlayfields.Value)
foreach (var p in nestedPlayfields)
p.SetKeepAlive(hitObject, keepAlive);
}
@ -444,10 +436,7 @@ namespace osu.Game.Rulesets.UI
foreach (var (_, entry) in lifetimeEntryMap)
entry.KeepAlive = true;
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var p in nestedPlayfields.Value)
foreach (var p in nestedPlayfields)
p.KeepAllAlive();
}
@ -461,10 +450,7 @@ namespace osu.Game.Rulesets.UI
{
HitObjectContainer.PastLifetimeExtension = value;
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var nested in nestedPlayfields.Value)
foreach (var nested in nestedPlayfields)
nested.PastLifetimeExtension = value;
}
}
@ -479,10 +465,7 @@ namespace osu.Game.Rulesets.UI
{
HitObjectContainer.FutureLifetimeExtension = value;
if (!nestedPlayfields.IsValueCreated)
return;
foreach (var nested in nestedPlayfields.Value)
foreach (var nested in nestedPlayfields)
nested.FutureLifetimeExtension = value;
}
}

View File

@ -27,9 +27,12 @@ namespace osu.Game.Screens.Backgrounds
private WorkingBeatmap beatmap;
/// <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>
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>();
@ -50,7 +53,7 @@ namespace osu.Game.Screens.Backgrounds
InternalChild = dimmable = CreateFadeContainer();
dimmable.EnableUserDim.BindTo(EnableUserDim);
dimmable.IgnoreUserSettings.BindTo(IgnoreUserSettings);
dimmable.IsBreakTime.BindTo(IsBreakTime);
dimmable.BlurAmount.BindTo(BlurAmount);
@ -148,7 +151,7 @@ namespace osu.Game.Screens.Backgrounds
/// <summary>
/// As an optimisation, we add the two blur portions to be applied rather than actually applying two separate blurs.
/// </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);
@ -166,7 +169,9 @@ namespace osu.Game.Screens.Backgrounds
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()
{

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
}
[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.
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
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
{
public class GroupVisualisation : PointVisualisation
public class GroupVisualisation : CompositeDrawable
{
[Resolved]
private OsuColour colours { get; set; }
public readonly ControlPointGroup Group;
private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
[Resolved]
private OsuColour colours { get; set; }
public GroupVisualisation(ControlPointGroup group)
: base(group.Time)
{
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both;
Origin = Anchor.TopLeft;
Group = group;
X = (float)group.Time;
}
protected override void LoadComplete()
@ -33,13 +37,32 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
controlPoints.BindTo(Group.ControlPoints);
controlPoints.BindCollectionChanged((_, __) =>
{
if (controlPoints.Count == 0)
{
Colour = Color4.Transparent;
return;
}
ClearInternal();
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);
}
}

View File

@ -27,6 +27,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Anchor = Anchor.Centre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Y = -10,
Height = 0.35f
},
new BookmarkPart
@ -38,6 +39,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
},
new Container
{
Name = "centre line",
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray5,
Children = new Drawable[]
@ -45,7 +47,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
new Circle
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Origin = Anchor.Centre,
Size = new Vector2(5)
},
new Box
@ -59,7 +61,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
new Circle
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
Origin = Anchor.Centre,
Size = new Vector2(5)
},
}
@ -69,7 +71,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
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.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
@ -10,19 +9,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
/// <summary>
/// Represents a spanning point on a timeline part.
/// </summary>
public class DurationVisualisation : Container
public class DurationVisualisation : Circle
{
protected DurationVisualisation(double startTime, double endTime)
{
Masking = true;
CornerRadius = 5;
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.Both;
X = (float)startTime;
Width = (float)(endTime - startTime);
AddInternal(new Box { RelativeSizeAxes = Axes.Both });
}
}
}

View File

@ -3,16 +3,15 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osuTK;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
{
/// <summary>
/// Represents a singular point on a timeline part.
/// </summary>
public class PointVisualisation : Box
public class PointVisualisation : Circle
{
public const float WIDTH = 1;
public const float MAX_WIDTH = 4;
public PointVisualisation(double startTime)
: this()
@ -22,13 +21,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
public PointVisualisation()
{
Origin = Anchor.TopCentre;
RelativePositionAxes = Axes.X;
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.Y;
Width = WIDTH;
EdgeSmoothness = new Vector2(WIDTH, 0);
Anchor = Anchor.CentreLeft;
Origin = Anchor.Centre;
Width = MAX_WIDTH;
Height = 0.75f;
}
}
}

View File

@ -135,11 +135,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!beginClickSelection(e)) return true;
bool selectionPerformed = performMouseDownActions(e);
// even if a selection didn't occur, a drag event may still move the selection.
prepareSelectionMovement();
return e.Button == MouseButton.Left;
return selectionPerformed || e.Button == MouseButton.Left;
}
private SelectionBlueprint clickedBlueprint;
@ -154,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Deselection should only occur if no selected blueprints are hovered
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
if (endClickSelection() || clickedBlueprint != null)
if (endClickSelection(e) || clickedBlueprint != null)
return true;
deselectAll();
@ -177,7 +178,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void OnMouseUp(MouseUpEvent e)
{
// Special case for when a drag happened instead of a click
Schedule(() => endClickSelection());
Schedule(() =>
{
endClickSelection(e);
clickSelectionBegan = false;
isDraggingBlueprint = false;
});
finishSelectionMovement();
}
@ -226,7 +232,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
Beatmap.Update(obj);
changeHandler?.EndChange();
isDraggingBlueprint = false;
}
if (DragBox.State == Visibility.Visible)
@ -338,7 +343,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
/// <param name="e">The input event that triggered this selection.</param>
/// <returns>Whether a selection was performed.</returns>
private bool beginClickSelection(MouseButtonEvent e)
private bool performMouseDownActions(MouseButtonEvent e)
{
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
// Priority is given to already-selected blueprints.
@ -346,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
if (!blueprint.IsHovered) continue;
return clickSelectionBegan = SelectionHandler.HandleSelectionRequested(blueprint, e);
return clickSelectionBegan = SelectionHandler.MouseDownSelectionRequested(blueprint, e);
}
return false;
@ -355,13 +360,28 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary>
/// Finishes the current blueprint selection.
/// </summary>
/// <param name="e">The mouse event which triggered end of selection.</param>
/// <returns>Whether a click selection was active.</returns>
private bool endClickSelection()
private bool endClickSelection(MouseButtonEvent e)
{
if (!clickSelectionBegan)
return false;
if (!clickSelectionBegan && !isDraggingBlueprint)
{
// if a selection didn't occur, we may want to trigger a deselection.
if (e.ControlPressed && e.Button == MouseButton.Left)
{
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
// Priority is given to already-selected blueprints.
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
{
if (!blueprint.IsHovered) continue;
return clickSelectionBegan = SelectionHandler.MouseUpSelectionRequested(blueprint, e);
}
}
return false;
}
clickSelectionBegan = false;
return true;
}

View File

@ -220,20 +220,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="blueprint">The blueprint.</param>
/// <param name="e">The mouse event responsible for selection.</param>
/// <returns>Whether a selection was performed.</returns>
internal bool HandleSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
internal bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
{
if (e.ShiftPressed && e.Button == MouseButton.Right)
{
handleQuickDeletion(blueprint);
return false;
return true;
}
if (e.ControlPressed && e.Button == MouseButton.Left)
// while holding control, we only want to add to selection, not replace an existing selection.
if (e.ControlPressed && e.Button == MouseButton.Left && !blueprint.IsSelected)
{
blueprint.ToggleSelection();
else
ensureSelected(blueprint);
return true;
}
return true;
return ensureSelected(blueprint);
}
/// <summary>
/// Handle a blueprint requesting selection.
/// </summary>
/// <param name="blueprint">The blueprint.</param>
/// <param name="e">The mouse event responsible for deselection.</param>
/// <returns>Whether a deselection was performed.</returns>
internal bool MouseUpSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e)
{
if (blueprint.IsSelected)
{
blueprint.ToggleSelection();
return true;
}
return false;
}
private void handleQuickDeletion(SelectionBlueprint blueprint)
@ -247,13 +266,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
deleteSelected();
}
private void ensureSelected(SelectionBlueprint blueprint)
/// <summary>
/// Ensure the blueprint is in a selected state.
/// </summary>
/// <param name="blueprint">The blueprint to select.</param>
/// <returns>Whether selection state was changed.</returns>
private bool ensureSelected(SelectionBlueprint blueprint)
{
if (blueprint.IsSelected)
return;
return false;
DeselectAll?.Invoke();
blueprint.Select();
return true;
}
private void deleteSelected()

View File

@ -63,7 +63,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
AddInternal(backgroundBox = new SelectableAreaBackground
{
Colour = Color4.Black
Colour = Color4.Black,
Depth = float.MaxValue,
});
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -16,7 +17,6 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -28,9 +28,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineHitObjectBlueprint : SelectionBlueprint
{
private const float thickness = 5;
private const float shadow_radius = 5;
private const float circle_size = 34;
private const float circle_size = 38;
private Container repeatsContainer;
public Action<DragEvent> OnDragHandled;
@ -40,10 +40,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private Bindable<int> indexInCurrentComboBindable;
private Bindable<int> comboIndexBindable;
private readonly Circle circle;
private readonly DragBar dragBar;
private readonly List<Container> shadowComponents = new List<Container>();
private readonly Container mainComponents;
private readonly Drawable circle;
private readonly Container colouredComponents;
private readonly OsuSpriteText comboIndexText;
[Resolved]
@ -61,89 +60,41 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativePositionAxes = Axes.X;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Height = circle_size;
AddRangeInternal(new Drawable[]
AddRangeInternal(new[]
{
mainComponents = new Container
circle = new ExtendableCircle
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
colouredComponents = new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
comboIndexText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black),
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
comboIndexText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
Y = -1,
Font = OsuFont.Default.With(size: circle_size * 0.5f, weight: FontWeight.Regular),
},
}
},
});
circle = new Circle
{
Size = new Vector2(circle_size),
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = shadow_radius,
Colour = Color4.Black
},
};
shadowComponents.Add(circle);
if (hitObject is IHasDuration)
{
DragBar dragBarUnderlay;
Container extensionBar;
mainComponents.AddRange(new Drawable[]
colouredComponents.Add(new DragArea(hitObject)
{
extensionBar = new Container
{
Masking = true,
Size = new Vector2(1, thickness),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativePositionAxes = Axes.X,
RelativeSizeAxes = Axes.X,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = shadow_radius,
Colour = Color4.Black
},
Child = new Box
{
RelativeSizeAxes = Axes.Both,
}
},
circle,
// only used for drawing the shadow
dragBarUnderlay = new DragBar(null),
// cover up the shadow on the join
new Box
{
Height = thickness,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
},
dragBar = new DragBar(hitObject) { OnDragHandled = e => OnDragHandled?.Invoke(e) },
OnDragHandled = e => OnDragHandled?.Invoke(e)
});
shadowComponents.Add(dragBarUnderlay);
shadowComponents.Add(extensionBar);
}
else
{
mainComponents.Add(circle);
}
updateShadows();
}
protected override void LoadComplete()
@ -162,6 +113,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
protected override void OnSelected()
{
// base logic hides selected blueprints when not selected, but timeline doesn't do that.
}
protected override void OnDeselected()
{
// base logic hides selected blueprints when not selected, but timeline doesn't do that.
}
private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
private void updateComboColour()
@ -173,15 +134,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
var comboColour = combo.GetComboColour(comboColours);
if (HitObject is IHasDuration)
mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White);
circle.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f));
else
mainComponents.Colour = comboColour;
circle.Colour = comboColour;
var col = mainComponents.Colour.TopLeft.Linear;
var col = circle.Colour.TopLeft.Linear;
float brightness = col.R + col.G + col.B;
// decide the combo index colour based on brightness?
comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White;
colouredComponents.Colour = OsuColour.Gray(brightness > 0.5f ? 0.2f : 0.9f);
}
protected override void Update()
@ -201,13 +162,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
private Container repeatsContainer;
private void updateRepeats(IHasRepeats repeats)
{
repeatsContainer?.Expire();
mainComponents.Add(repeatsContainer = new Container
colouredComponents.Add(repeatsContainer = new Container
{
RelativeSizeAxes = Axes.Both,
});
@ -216,7 +175,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
repeatsContainer.Add(new Circle
{
Size = new Vector2(circle_size / 2),
Size = new Vector2(circle_size / 3),
Alpha = 0.2f,
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.X,
@ -228,61 +188,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override bool ShouldBeConsideredForInput(Drawable child) => true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
base.ReceivePositionalInputAt(screenSpacePos) ||
circle.ReceivePositionalInputAt(screenSpacePos) ||
dragBar?.ReceivePositionalInputAt(screenSpacePos) == true;
circle.ReceivePositionalInputAt(screenSpacePos);
protected override void OnSelected()
{
updateShadows();
}
private void updateShadows()
{
foreach (var s in shadowComponents)
{
if (State == SelectionState.Selected)
{
s.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = shadow_radius / 2,
Colour = Color4.Orange,
};
}
else
{
s.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = shadow_radius,
Colour = State == SelectionState.Selected ? Color4.Orange : Color4.Black
};
}
}
}
protected override void OnDeselected()
{
updateShadows();
}
public override Quad SelectionQuad
{
get
{
// correctly include the circle in the selection quad region, as it is usually outside the blueprint itself.
var leftQuad = circle.ScreenSpaceDrawQuad;
var rightQuad = dragBar?.ScreenSpaceDrawQuad ?? ScreenSpaceDrawQuad;
return new Quad(leftQuad.TopLeft, Vector2.ComponentMax(rightQuad.TopRight, leftQuad.TopRight),
leftQuad.BottomLeft, Vector2.ComponentMax(rightQuad.BottomRight, leftQuad.BottomRight));
}
}
public override Quad SelectionQuad => circle.ScreenSpaceDrawQuad;
public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;
public class DragBar : Container
public class DragArea : Circle
{
private readonly HitObject hitObject;
@ -293,13 +205,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public override bool HandlePositionalInput => hitObject != null;
public DragBar(HitObject hitObject)
public DragArea(HitObject hitObject)
{
this.hitObject = hitObject;
CornerRadius = 2;
CornerRadius = circle_size / 2;
Masking = true;
Size = new Vector2(5, 1);
Size = new Vector2(circle_size, 1);
Anchor = Anchor.CentreRight;
Origin = Anchor.Centre;
RelativePositionAxes = Axes.X;
@ -314,6 +226,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
FinishTransforms();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
@ -345,7 +265,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateState()
{
Colour = IsHovered || hasMouseDown ? Color4.OrangeRed : Color4.White;
if (hasMouseDown)
{
this.ScaleTo(0.7f, 200, Easing.OutQuint);
}
else if (IsHovered)
{
this.ScaleTo(0.8f, 200, Easing.OutQuint);
}
else
{
this.ScaleTo(0.6f, 200, Easing.OutQuint);
}
this.FadeTo(IsHovered || hasMouseDown ? 0.8f : 0.2f, 200, Easing.OutQuint);
}
[Resolved]
@ -406,5 +339,37 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
changeHandler?.EndChange();
}
}
/// <summary>
/// A circle with externalised end caps so it can take up the full width of a relative width area.
/// </summary>
public class ExtendableCircle : CompositeDrawable
{
private readonly CircularContainer content;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => content.ReceivePositionalInputAt(screenSpacePos);
public override Quad ScreenSpaceDrawQuad => content.ScreenSpaceDrawQuad;
public ExtendableCircle()
{
Padding = new MarginPadding { Horizontal = -circle_size / 2f };
InternalChild = content = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 5,
Colour = Color4.Black.Opacity(0.4f)
},
Child = new Box
{
RelativeSizeAxes = Axes.Both
}
};
}
}
}
}

View File

@ -6,9 +6,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@ -33,6 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private OsuColour colours { get; set; }
private static readonly int highest_divisor = BindableBeatDivisor.VALID_DIVISORS.Last();
public TimelineTickDisplay()
{
RelativeSizeAxes = Axes.Both;
@ -80,8 +80,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (timeline != null)
{
var newRange = (
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
if (visibleRange != newRange)
{
@ -100,7 +100,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void createTicks()
{
int drawableIndex = 0;
int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last();
nextMinTick = null;
nextMaxTick = null;
@ -131,25 +130,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
bool isMainBeat = indexInBar == 0;
// even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
float height = isMainBeat ? 0.5f : 0.4f - (float)divisor / highestDivisor * 0.2f;
float gradientOpacity = isMainBeat ? 1 : 0;
var topPoint = getNextUsablePoint();
topPoint.X = xPos;
topPoint.Height = height;
topPoint.Colour = ColourInfo.GradientVertical(colour, colour.Opacity(gradientOpacity));
topPoint.Anchor = Anchor.TopLeft;
topPoint.Origin = Anchor.TopCentre;
var bottomPoint = getNextUsablePoint();
bottomPoint.X = xPos;
bottomPoint.Anchor = Anchor.BottomLeft;
bottomPoint.Colour = ColourInfo.GradientVertical(colour.Opacity(gradientOpacity), colour);
bottomPoint.Origin = Anchor.BottomCentre;
bottomPoint.Height = height;
var line = getNextUsableLine();
line.X = xPos;
line.Width = PointVisualisation.MAX_WIDTH * getWidth(indexInBar, divisor);
line.Height = 0.9f * getHeight(indexInBar, divisor);
line.Colour = colour;
}
beat++;
@ -168,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
tickCache.Validate();
Drawable getNextUsablePoint()
Drawable getNextUsableLine()
{
PointVisualisation point;
if (drawableIndex >= Count)
@ -183,6 +170,54 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
private static float getWidth(int indexInBar, int divisor)
{
if (indexInBar == 0)
return 1;
switch (divisor)
{
case 1:
case 2:
return 0.6f;
case 3:
case 4:
return 0.5f;
case 6:
case 8:
return 0.4f;
default:
return 0.3f;
}
}
private static float getHeight(int indexInBar, int divisor)
{
if (indexInBar == 0)
return 1;
switch (divisor)
{
case 1:
case 2:
return 0.9f;
case 3:
case 4:
return 0.8f;
case 6:
case 8:
return 0.7f;
default:
return 0.6f;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@ -35,6 +35,7 @@ using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Design;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Screens.Edit.Verify;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK.Graphics;
@ -444,6 +445,10 @@ namespace osu.Game.Screens.Edit
menuBar.Mode.Value = EditorScreenMode.SongSetup;
return true;
case GlobalAction.EditorVerifyMode:
menuBar.Mode.Value = EditorScreenMode.Verify;
return true;
default:
return false;
}
@ -462,7 +467,7 @@ namespace osu.Game.Screens.Edit
// todo: temporary. we want to be applying dim using the UserDimContainer eventually.
b.FadeColour(Color4.DarkGray, 500);
b.EnableUserDim.Value = false;
b.IgnoreUserSettings.Value = true;
b.BlurAmount.Value = 0;
});
@ -631,6 +636,10 @@ namespace osu.Game.Screens.Edit
case EditorScreenMode.Timing:
currentScreen = new TimingScreen();
break;
case EditorScreenMode.Verify:
currentScreen = new VerifyScreen();
break;
}
LoadComponentAsync(currentScreen, newScreen =>

View File

@ -18,5 +18,8 @@ namespace osu.Game.Screens.Edit
[Description("timing")]
Timing,
[Description("verify")]
Verify,
}
}

View File

@ -0,0 +1,140 @@
// 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.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit
{
public abstract class EditorTable : TableContainer
{
private const float horizontal_inset = 20;
protected const float ROW_HEIGHT = 25;
protected const int TEXT_SIZE = 14;
protected readonly FillFlowContainer<RowBackground> BackgroundFlow;
protected EditorTable()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Horizontal = horizontal_inset };
RowSize = new Dimension(GridSizeMode.Absolute, ROW_HEIGHT);
AddInternal(BackgroundFlow = new FillFlowContainer<RowBackground>
{
RelativeSizeAxes = Axes.Both,
Depth = 1f,
Padding = new MarginPadding { Horizontal = -horizontal_inset },
Margin = new MarginPadding { Top = ROW_HEIGHT }
});
}
protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
private class HeaderText : OsuSpriteText
{
public HeaderText(string text)
{
Text = text.ToUpper();
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);
}
}
public class RowBackground : OsuClickableContainer
{
public readonly object Item;
private const int fade_duration = 100;
private readonly Box hoveredBackground;
[Resolved]
private EditorClock clock { get; set; }
public RowBackground(object item)
{
Item = item;
RelativeSizeAxes = Axes.X;
Height = 25;
AlwaysPresent = true;
CornerRadius = 3;
Masking = true;
Children = new Drawable[]
{
hoveredBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
};
// todo delete
Action = () =>
{
};
}
private Color4 colourHover;
private Color4 colourSelected;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
hoveredBackground.Colour = colourHover = colours.BlueDarker;
colourSelected = colours.YellowDarker;
}
private bool selected;
public bool Selected
{
get => selected;
set
{
if (value == selected)
return;
selected = value;
updateState();
}
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
if (selected || IsHovered)
hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
else
hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
}
}
}
}

View File

@ -8,59 +8,43 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Timing
{
public class ControlPointTable : TableContainer
public class ControlPointTable : EditorTable
{
private const float horizontal_inset = 20;
private const float row_height = 25;
private const int text_size = 14;
private readonly FillFlowContainer backgroundFlow;
[Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; }
public ControlPointTable()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Horizontal = horizontal_inset };
RowSize = new Dimension(GridSizeMode.Absolute, row_height);
AddInternal(backgroundFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Depth = 1f,
Padding = new MarginPadding { Horizontal = -horizontal_inset },
Margin = new MarginPadding { Top = row_height }
});
}
[Resolved]
private EditorClock clock { get; set; }
public IEnumerable<ControlPointGroup> ControlGroups
{
set
{
Content = null;
backgroundFlow.Clear();
BackgroundFlow.Clear();
if (value?.Any() != true)
return;
foreach (var group in value)
{
backgroundFlow.Add(new RowBackground(group));
BackgroundFlow.Add(new RowBackground(group)
{
Action = () =>
{
selectedGroup.Value = group;
clock.SeekSmoothlyTo(group.Time);
}
});
}
Columns = createHeaders();
@ -68,6 +52,16 @@ namespace osu.Game.Screens.Edit.Timing
}
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedGroup.BindValueChanged(group =>
{
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
}, true);
}
private TableColumn[] createHeaders()
{
var columns = new List<TableColumn>
@ -86,13 +80,13 @@ namespace osu.Game.Screens.Edit.Timing
new OsuSpriteText
{
Text = $"#{index + 1}",
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Margin = new MarginPadding(10)
},
new OsuSpriteText
{
Text = group.Time.ToEditorFormattedString(),
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold)
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold)
},
null,
new ControlGroupAttributes(group),
@ -163,111 +157,5 @@ namespace osu.Game.Screens.Edit.Timing
return null;
}
}
protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
private class HeaderText : OsuSpriteText
{
public HeaderText(string text)
{
Text = text.ToUpper();
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);
}
}
public class RowBackground : OsuClickableContainer
{
private readonly ControlPointGroup controlGroup;
private const int fade_duration = 100;
private readonly Box hoveredBackground;
[Resolved]
private EditorClock clock { get; set; }
[Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; }
public RowBackground(ControlPointGroup controlGroup)
{
this.controlGroup = controlGroup;
RelativeSizeAxes = Axes.X;
Height = 25;
AlwaysPresent = true;
CornerRadius = 3;
Masking = true;
Children = new Drawable[]
{
hoveredBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
};
Action = () =>
{
selectedGroup.Value = controlGroup;
clock.SeekSmoothlyTo(controlGroup.Time);
};
}
private Color4 colourHover;
private Color4 colourSelected;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
hoveredBackground.Colour = colourHover = colours.BlueDarker;
colourSelected = colours.YellowDarker;
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedGroup.BindValueChanged(group => { Selected = controlGroup == group.NewValue; }, true);
}
private bool selected;
protected bool Selected
{
get => selected;
set
{
if (value == selected)
return;
selected = value;
updateState();
}
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
if (selected || IsHovered)
hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
else
hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
}
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.Edit.Verify
{
public class IssueSettings : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Gray3,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = createSections()
},
}
};
}
private IReadOnlyList<Drawable> createSections() => new Drawable[]
{
};
}
}

View File

@ -0,0 +1,128 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Screens.Edit.Verify
{
public class IssueTable : EditorTable
{
[Resolved]
private Bindable<Issue> selectedIssue { get; set; }
[Resolved]
private EditorClock clock { get; set; }
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
[Resolved]
private Editor editor { get; set; }
public IEnumerable<Issue> Issues
{
set
{
Content = null;
BackgroundFlow.Clear();
if (value == null)
return;
foreach (var issue in value)
{
BackgroundFlow.Add(new RowBackground(issue)
{
Action = () =>
{
selectedIssue.Value = issue;
if (issue.Time != null)
{
clock.Seek(issue.Time.Value);
editor.OnPressed(GlobalAction.EditorComposeMode);
}
if (!issue.HitObjects.Any())
return;
editorBeatmap.SelectedHitObjects.Clear();
editorBeatmap.SelectedHitObjects.AddRange(issue.HitObjects);
},
});
}
Columns = createHeaders();
Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular();
}
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedIssue.BindValueChanged(issue =>
{
foreach (var b in BackgroundFlow) b.Selected = b.Item == issue.NewValue;
}, true);
}
private TableColumn[] createHeaders()
{
var columns = new List<TableColumn>
{
new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Type", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),
new TableColumn("Time", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),
new TableColumn("Message", Anchor.CentreLeft),
new TableColumn("Category", Anchor.CentreRight, new Dimension(GridSizeMode.AutoSize)),
};
return columns.ToArray();
}
private Drawable[] createContent(int index, Issue issue) => new Drawable[]
{
new OsuSpriteText
{
Text = $"#{index + 1}",
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),
Margin = new MarginPadding { Right = 10 }
},
new OsuSpriteText
{
Text = issue.Template.Type.ToString(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Margin = new MarginPadding { Right = 10 },
Colour = issue.Template.Colour
},
new OsuSpriteText
{
Text = issue.GetEditorTimestamp(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Margin = new MarginPadding { Right = 10 },
},
new OsuSpriteText
{
Text = issue.ToString(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium)
},
new OsuSpriteText
{
Text = issue.Check.Metadata.Category.ToString(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Margin = new MarginPadding(10)
}
};
}
}

View File

@ -0,0 +1,133 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osuTK;
namespace osu.Game.Screens.Edit.Verify
{
public class VerifyScreen : EditorScreen
{
[Cached]
private Bindable<Issue> selectedIssue = new Bindable<Issue>();
public VerifyScreen()
: base(EditorScreenMode.Verify)
{
}
[BackgroundDependencyLoader]
private void load()
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 200),
},
Content = new[]
{
new Drawable[]
{
new IssueList(),
new IssueSettings(),
},
}
}
};
}
public class IssueList : CompositeDrawable
{
private IssueTable table;
[Resolved]
private EditorClock clock { get; set; }
[Resolved]
protected EditorBeatmap Beatmap { get; private set; }
[Resolved]
private Bindable<Issue> selectedIssue { get; set; }
private IBeatmapVerifier rulesetVerifier;
private BeatmapVerifier generalVerifier;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
generalVerifier = new BeatmapVerifier();
rulesetVerifier = Beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier();
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Gray0,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = table = new IssueTable(),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding(20),
Children = new Drawable[]
{
new TriangleButton
{
Text = "Refresh",
Action = refresh,
Size = new Vector2(120, 40),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
refresh();
}
private void refresh()
{
var issues = generalVerifier.Run(Beatmap);
if (rulesetVerifier != null)
issues = issues.Concat(rulesetVerifier.Run(Beatmap));
table.Issues = issues
.OrderBy(issue => issue.Template.Type)
.ThenBy(issue => issue.Check.Metadata.Category);
}
}
}
}

View File

@ -19,6 +19,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected OnlinePlayComposite Settings { get; set; }
protected override bool BlockScrollInput => false;
[BackgroundDependencyLoader]
private void load()
{

View File

@ -0,0 +1,167 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// A grid of players playing the multiplayer match.
/// </summary>
public partial class PlayerGrid : CompositeDrawable
{
private const float player_spacing = 5;
/// <summary>
/// The currently-maximised facade.
/// </summary>
public Drawable MaximisedFacade => maximisedFacade;
private readonly Facade maximisedFacade;
private readonly Container paddingContainer;
private readonly FillFlowContainer<Facade> facadeContainer;
private readonly Container<Cell> cellContainer;
public PlayerGrid()
{
InternalChildren = new Drawable[]
{
paddingContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(player_spacing),
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Child = facadeContainer = new FillFlowContainer<Facade>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(player_spacing),
}
},
maximisedFacade = new Facade { RelativeSizeAxes = Axes.Both }
}
},
cellContainer = new Container<Cell> { RelativeSizeAxes = Axes.Both }
};
}
/// <summary>
/// Adds a new cell with content to this grid.
/// </summary>
/// <param name="content">The content the cell should contain.</param>
/// <exception cref="InvalidOperationException">If more than 16 cells are added.</exception>
public void Add(Drawable content)
{
if (cellContainer.Count == 16)
throw new InvalidOperationException("Only 16 cells are supported.");
int index = cellContainer.Count;
var facade = new Facade();
facadeContainer.Add(facade);
var cell = new Cell(index, content) { ToggleMaximisationState = toggleMaximisationState };
cell.SetFacade(facade);
cellContainer.Add(cell);
}
/// <summary>
/// The content added to this grid.
/// </summary>
public IEnumerable<Drawable> Content => cellContainer.OrderBy(c => c.FacadeIndex).Select(c => c.Content);
// A depth value that gets decremented every time a new instance is maximised in order to reduce underlaps.
private float maximisedInstanceDepth;
private void toggleMaximisationState(Cell target)
{
// Iterate through all cells to ensure only one is maximised at any time.
foreach (var i in cellContainer.ToList())
{
if (i == target)
i.IsMaximised = !i.IsMaximised;
else
i.IsMaximised = false;
if (i.IsMaximised)
{
// Transfer cell to the maximised facade.
i.SetFacade(maximisedFacade);
cellContainer.ChangeChildDepth(i, maximisedInstanceDepth -= 0.001f);
}
else
{
// Transfer cell back to its original facade.
i.SetFacade(facadeContainer[i.FacadeIndex]);
}
}
}
protected override void Update()
{
base.Update();
// Different layouts are used for varying cell counts in order to maximise dimensions.
Vector2 cellsPerDimension;
switch (facadeContainer.Count)
{
case 1:
cellsPerDimension = Vector2.One;
break;
case 2:
cellsPerDimension = new Vector2(2, 1);
break;
case 3:
case 4:
cellsPerDimension = new Vector2(2);
break;
case 5:
case 6:
cellsPerDimension = new Vector2(3, 2);
break;
case 7:
case 8:
case 9:
// 3 rows / 3 cols.
cellsPerDimension = new Vector2(3);
break;
case 10:
case 11:
case 12:
// 3 rows / 4 cols.
cellsPerDimension = new Vector2(4, 3);
break;
default:
// 4 rows / 4 cols.
cellsPerDimension = new Vector2(4);
break;
}
// Total inter-cell spacing.
Vector2 totalCellSpacing = player_spacing * (cellsPerDimension - Vector2.One);
Vector2 fullSize = paddingContainer.ChildSize - totalCellSpacing;
Vector2 cellSize = Vector2.Divide(fullSize, new Vector2(cellsPerDimension.X, cellsPerDimension.Y));
foreach (var cell in facadeContainer)
cell.Size = cellSize;
}
}
}

View File

@ -0,0 +1,99 @@
// 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 JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
public partial class PlayerGrid
{
/// <summary>
/// A cell of the grid. Contains the content and tracks to the linked facade.
/// </summary>
private class Cell : CompositeDrawable
{
/// <summary>
/// The index of the original facade of this cell.
/// </summary>
public readonly int FacadeIndex;
/// <summary>
/// The contained content.
/// </summary>
public readonly Drawable Content;
/// <summary>
/// An action that toggles the maximisation state of this cell.
/// </summary>
public Action<Cell> ToggleMaximisationState;
/// <summary>
/// Whether this cell is currently maximised.
/// </summary>
public bool IsMaximised;
private Facade facade;
private bool isTracking = true;
public Cell(int facadeIndex, Drawable content)
{
FacadeIndex = facadeIndex;
Origin = Anchor.Centre;
InternalChild = Content = content;
}
protected override void Update()
{
base.Update();
if (isTracking)
{
Position = getFinalPosition();
Size = getFinalSize();
}
}
/// <summary>
/// Makes this cell track a new facade.
/// </summary>
public void SetFacade([NotNull] Facade newFacade)
{
Facade lastFacade = facade;
facade = newFacade;
if (lastFacade == null || lastFacade == newFacade)
return;
isTracking = false;
this.MoveTo(getFinalPosition(), 400, Easing.OutQuint).ResizeTo(getFinalSize(), 400, Easing.OutQuint)
.Then()
.OnComplete(_ =>
{
if (facade == newFacade)
isTracking = true;
});
}
private Vector2 getFinalPosition()
{
var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero));
return topLeft + facade.DrawSize / 2;
}
private Vector2 getFinalSize() => facade.DrawSize;
protected override bool OnClick(ClickEvent e)
{
ToggleMaximisationState(this);
return true;
}
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
public partial class PlayerGrid
{
/// <summary>
/// A facade of the grid which is used as a dummy object to store the required position/size of cells.
/// </summary>
private class Facade : Drawable
{
public Facade()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
}
}
}

View File

@ -31,6 +31,8 @@ namespace osu.Game.Screens.Play
protected override bool BlockNonPositionalInput => true;
protected override bool BlockScrollInput => false;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public Action OnRetry;

View File

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

View File

@ -24,6 +24,7 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@ -112,6 +113,9 @@ namespace osu.Game.Screens.Play
[Resolved]
private AudioManager audioManager { get; set; }
[Resolved(CanBeNull = true)]
private BatteryInfo batteryInfo { get; set; }
public PlayerLoader(Func<Player> createPlayer)
{
this.createPlayer = createPlayer;
@ -121,6 +125,7 @@ namespace osu.Game.Screens.Play
private void load(SessionStatics sessionStatics)
{
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
InternalChild = (content = new LogoTrackingContainer
{
@ -196,6 +201,7 @@ namespace osu.Game.Screens.Play
Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0));
showMuteWarningIfNeeded();
showBatteryWarningIfNeeded();
}
public override void OnResuming(IScreen last)
@ -229,7 +235,7 @@ namespace osu.Game.Screens.Play
content.ScaleTo(0.7f, 150, Easing.InQuint);
this.FadeOut(150);
ApplyToBackground(b => b.EnableUserDim.Value = false);
ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
BackgroundBrightnessReduction = false;
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.
ApplyToBackground(b =>
{
b.EnableUserDim.Value = true;
b.IgnoreUserSettings.Value = false;
b.BlurAmount.Value = 0;
});
@ -288,7 +294,7 @@ namespace osu.Game.Screens.Play
ApplyToBackground(b =>
{
// Returns background dim and blur to the values specified by PlayerLoader.
b.EnableUserDim.Value = false;
b.IgnoreUserSettings.Value = true;
b.BlurAmount.Value = BACKGROUND_BLUR;
});
@ -470,5 +476,48 @@ namespace osu.Game.Screens.Play
}
#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

@ -45,6 +45,8 @@ namespace osu.Game.Screens.Play
public override bool HandleNonPositionalInput => AllowSeeking.Value;
public override bool HandlePositionalInput => AllowSeeking.Value;
protected override bool BlockScrollInput => false;
private double firstHitTime => objects.First().StartTime;
private IEnumerable<HitObject> objects;

View File

@ -109,7 +109,12 @@ namespace osu.Game.Screens.Play
request.Success += s =>
{
score.ScoreInfo.OnlineScoreID = s.ID;
// For the time being, online ID responses are not really useful for anything.
// In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores.
//
// Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
// conflicts across various systems (ie. solo and multiplayer).
// score.ScoreInfo.OnlineScoreID = s.ID;
tcs.SetResult(true);
};

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

@ -3,8 +3,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Rulesets.Mods;
#nullable enable
@ -129,5 +132,38 @@ namespace osu.Game.Utils
else
yield return mod;
}
/// <summary>
/// Returns the underlying value of the given mod setting object.
/// Used in <see cref="APIMod"/> for serialization and equality comparison purposes.
/// </summary>
/// <param name="setting">The mod setting.</param>
public static object GetSettingUnderlyingValue(object setting)
{
switch (setting)
{
case Bindable<double> d:
return d.Value;
case Bindable<int> i:
return i.Value;
case Bindable<float> f:
return f.Value;
case Bindable<bool> b:
return b.Value;
case IBindable u:
// A mod with unknown (e.g. enum) generic type.
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
Debug.Assert(valueMethod != null);
return valueMethod.GetValue(u);
default:
// fall back for non-bindable cases.
return setting;
}
}
}
}

View File

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

View File

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

View File

@ -5,6 +5,8 @@ using System;
using Foundation;
using osu.Game;
using osu.Game.Updater;
using osu.Game.Utils;
using Xamarin.Essentials;
namespace osu.iOS
{
@ -13,5 +15,14 @@ namespace osu.iOS
public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString());
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>
</ImageAsset>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Essentials" Version="1.6.1" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
</Project>