mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 09:32:55 +08:00
Merge branch 'fix-initial-spectator-state-callback' into multiplayer-spectator-screen
This commit is contained in:
commit
65a6f9f8a4
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
115
osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs
Normal file
115
osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
22
osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
Normal file
22
osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
Normal 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));
|
||||
}
|
||||
}
|
44
osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
Normal file
44
osu.Game.Rulesets.Osu/Mods/OsuModBarrelRoll.cs
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModBarrelRoll : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
{
|
||||
[SettingSource("Roll speed", "Rotations per minute")]
|
||||
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
|
||||
{
|
||||
MinValue = 0.02,
|
||||
MaxValue = 4,
|
||||
Precision = 0.01,
|
||||
};
|
||||
|
||||
[SettingSource("Direction", "The direction of rotation")]
|
||||
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
|
||||
|
||||
public override string Name => "Barrel Roll";
|
||||
public override string Acronym => "BR";
|
||||
public override string Description => "The whole playfield is on a wheel!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
playfield.Rotation = (Direction.Value == RotationDirection.CounterClockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// scale the playfield to allow all hitobjects to stay within the visible region.
|
||||
drawableRuleset.Playfield.Scale = new Vector2(OsuPlayfield.BASE_SIZE.Y / OsuPlayfield.BASE_SIZE.X);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -185,6 +185,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new MultiMod(new OsuModGrow(), new OsuModDeflate()),
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new OsuModTraceable(),
|
||||
new OsuModBarrelRoll(),
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
@ -206,6 +207,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;
|
||||
|
@ -42,6 +42,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
public OsuPlayfield()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
|
||||
|
@ -33,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
Add(cursorScaleContainer = new Container
|
||||
{
|
||||
RelativePositionAxes = Axes.Both,
|
||||
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume }
|
||||
});
|
||||
}
|
||||
|
67
osu.Game.Tests/Editing/Checks/CheckBackgroundTest.cs
Normal file
67
osu.Game.Tests/Editing/Checks/CheckBackgroundTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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) };
|
||||
|
@ -20,27 +20,14 @@ namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
handler = new TestInputHandler(replay = new Replay
|
||||
{
|
||||
Frames = new List<ReplayFrame>
|
||||
{
|
||||
new TestReplayFrame(0),
|
||||
new TestReplayFrame(1000),
|
||||
new TestReplayFrame(2000),
|
||||
new TestReplayFrame(3000, true),
|
||||
new TestReplayFrame(4000, true),
|
||||
new TestReplayFrame(5000, true),
|
||||
new TestReplayFrame(7000, true),
|
||||
new TestReplayFrame(8000),
|
||||
}
|
||||
HasReceivedAllFrames = false
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalPlayback()
|
||||
{
|
||||
Assert.IsNull(handler.CurrentFrame);
|
||||
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
setReplayFrames();
|
||||
|
||||
setTime(0, 0);
|
||||
confirmCurrentFrame(0);
|
||||
@ -107,6 +94,8 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestIntroTime()
|
||||
{
|
||||
setReplayFrames();
|
||||
|
||||
setTime(-1000, -1000);
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
@ -123,6 +112,8 @@ namespace osu.Game.Tests.NonVisual
|
||||
[Test]
|
||||
public void TestBasicRewind()
|
||||
{
|
||||
setReplayFrames();
|
||||
|
||||
setTime(2800, 0);
|
||||
setTime(2800, 1000);
|
||||
setTime(2800, 2000);
|
||||
@ -133,34 +124,35 @@ namespace osu.Game.Tests.NonVisual
|
||||
// pivot without crossing a frame boundary
|
||||
setTime(2700, 2700);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(1);
|
||||
confirmNextFrame(3);
|
||||
|
||||
// cross current frame boundary; should not yet update frame
|
||||
setTime(1980, 1980);
|
||||
// cross current frame boundary
|
||||
setTime(1980, 2000);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(1);
|
||||
confirmNextFrame(3);
|
||||
|
||||
setTime(1200, 1200);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(1);
|
||||
confirmCurrentFrame(1);
|
||||
confirmNextFrame(2);
|
||||
|
||||
// ensure each frame plays out until start
|
||||
setTime(-500, 1000);
|
||||
confirmCurrentFrame(1);
|
||||
confirmNextFrame(0);
|
||||
confirmNextFrame(2);
|
||||
|
||||
setTime(-500, 0);
|
||||
confirmCurrentFrame(0);
|
||||
confirmNextFrame(null);
|
||||
confirmNextFrame(1);
|
||||
|
||||
setTime(-500, -500);
|
||||
confirmCurrentFrame(0);
|
||||
confirmNextFrame(null);
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindInsideImportantSection()
|
||||
{
|
||||
setReplayFrames();
|
||||
fastForwardToPoint(3000);
|
||||
|
||||
setTime(4000, 4000);
|
||||
@ -168,12 +160,12 @@ namespace osu.Game.Tests.NonVisual
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(3);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3000, 3000);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(2);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(3);
|
||||
@ -187,46 +179,127 @@ namespace osu.Game.Tests.NonVisual
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(4000, null);
|
||||
setTime(4000, 4000);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(3);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3000, 3000);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(2);
|
||||
confirmNextFrame(4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindOutOfImportantSection()
|
||||
{
|
||||
setReplayFrames();
|
||||
fastForwardToPoint(3500);
|
||||
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3200, null);
|
||||
// next frame doesn't change even though direction reversed, because of important section.
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3000, null);
|
||||
setTime(3000, 3000);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(2800, 2800);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(2);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReplayStreaming()
|
||||
{
|
||||
// no frames are arrived yet
|
||||
setTime(0, null);
|
||||
setTime(1000, null);
|
||||
Assert.IsTrue(handler.WaitingForFrame, "Should be waiting for the first frame");
|
||||
|
||||
replay.Frames.Add(new TestReplayFrame(0));
|
||||
replay.Frames.Add(new TestReplayFrame(1000));
|
||||
|
||||
// should always play from beginning
|
||||
setTime(1000, 0);
|
||||
confirmCurrentFrame(0);
|
||||
Assert.IsFalse(handler.WaitingForFrame, "Should not be waiting yet");
|
||||
setTime(1000, 1000);
|
||||
confirmCurrentFrame(1);
|
||||
confirmNextFrame(null);
|
||||
Assert.IsTrue(handler.WaitingForFrame, "Should be waiting");
|
||||
|
||||
// cannot seek beyond the last frame
|
||||
setTime(1500, null);
|
||||
confirmCurrentFrame(1);
|
||||
|
||||
setTime(-100, 0);
|
||||
confirmCurrentFrame(0);
|
||||
|
||||
// can seek to the point before the first frame, however
|
||||
setTime(-100, -100);
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
|
||||
fastForwardToPoint(1000);
|
||||
setTime(3000, null);
|
||||
replay.Frames.Add(new TestReplayFrame(2000));
|
||||
confirmCurrentFrame(1);
|
||||
setTime(1000, 1000);
|
||||
setTime(3000, 2000);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleFramesSameTime()
|
||||
{
|
||||
replay.Frames.Add(new TestReplayFrame(0));
|
||||
replay.Frames.Add(new TestReplayFrame(0));
|
||||
replay.Frames.Add(new TestReplayFrame(1000));
|
||||
replay.Frames.Add(new TestReplayFrame(1000));
|
||||
replay.Frames.Add(new TestReplayFrame(2000));
|
||||
|
||||
// forward direction is prioritized when multiple frames have the same time.
|
||||
setTime(0, 0);
|
||||
setTime(0, 0);
|
||||
|
||||
setTime(2000, 1000);
|
||||
setTime(2000, 1000);
|
||||
|
||||
setTime(1000, 1000);
|
||||
setTime(1000, 1000);
|
||||
setTime(-100, 1000);
|
||||
setTime(-100, 0);
|
||||
setTime(-100, 0);
|
||||
setTime(-100, -100);
|
||||
}
|
||||
|
||||
private void setReplayFrames()
|
||||
{
|
||||
replay.Frames = new List<ReplayFrame>
|
||||
{
|
||||
new TestReplayFrame(0),
|
||||
new TestReplayFrame(1000),
|
||||
new TestReplayFrame(2000),
|
||||
new TestReplayFrame(3000, true),
|
||||
new TestReplayFrame(4000, true),
|
||||
new TestReplayFrame(5000, true),
|
||||
new TestReplayFrame(7000, true),
|
||||
new TestReplayFrame(8000),
|
||||
};
|
||||
replay.HasReceivedAllFrames = true;
|
||||
}
|
||||
|
||||
private void fastForwardToPoint(double destination)
|
||||
{
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
if (handler.SetFrameFromTime(destination) == null)
|
||||
var time = handler.SetFrameFromTime(destination);
|
||||
if (time == null || time == destination)
|
||||
return;
|
||||
}
|
||||
|
||||
@ -235,33 +308,17 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
private void setTime(double set, double? expect)
|
||||
{
|
||||
Assert.AreEqual(expect, handler.SetFrameFromTime(set));
|
||||
Assert.AreEqual(expect, handler.SetFrameFromTime(set), "Unexpected return value");
|
||||
}
|
||||
|
||||
private void confirmCurrentFrame(int? frame)
|
||||
{
|
||||
if (frame.HasValue)
|
||||
{
|
||||
Assert.IsNotNull(handler.CurrentFrame);
|
||||
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(handler.CurrentFrame);
|
||||
}
|
||||
Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.CurrentFrame?.Time, "Unexpected current frame");
|
||||
}
|
||||
|
||||
private void confirmNextFrame(int? frame)
|
||||
{
|
||||
if (frame.HasValue)
|
||||
{
|
||||
Assert.IsNotNull(handler.NextFrame);
|
||||
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(handler.NextFrame);
|
||||
}
|
||||
Assert.AreEqual(frame is int x ? replay.Frames[x].Time : (double?)null, handler.NextFrame?.Time, "Unexpected next frame");
|
||||
}
|
||||
|
||||
private class TestReplayFrame : ReplayFrame
|
||||
|
@ -1,296 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
[TestFixture]
|
||||
public class StreamingFramedReplayInputHandlerTest
|
||||
{
|
||||
private Replay replay;
|
||||
private TestInputHandler handler;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
handler = new TestInputHandler(replay = new Replay
|
||||
{
|
||||
HasReceivedAllFrames = false,
|
||||
Frames = new List<ReplayFrame>
|
||||
{
|
||||
new TestReplayFrame(0),
|
||||
new TestReplayFrame(1000),
|
||||
new TestReplayFrame(2000),
|
||||
new TestReplayFrame(3000, true),
|
||||
new TestReplayFrame(4000, true),
|
||||
new TestReplayFrame(5000, true),
|
||||
new TestReplayFrame(7000, true),
|
||||
new TestReplayFrame(8000),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalPlayback()
|
||||
{
|
||||
Assert.IsNull(handler.CurrentFrame);
|
||||
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
|
||||
setTime(0, 0);
|
||||
confirmCurrentFrame(0);
|
||||
confirmNextFrame(1);
|
||||
|
||||
// if we hit the first frame perfectly, time should progress to it.
|
||||
setTime(1000, 1000);
|
||||
confirmCurrentFrame(1);
|
||||
confirmNextFrame(2);
|
||||
|
||||
// in between non-important frames should progress based on input.
|
||||
setTime(1200, 1200);
|
||||
confirmCurrentFrame(1);
|
||||
|
||||
setTime(1400, 1400);
|
||||
confirmCurrentFrame(1);
|
||||
|
||||
// progressing beyond the next frame should force time to that frame once.
|
||||
setTime(2200, 2000);
|
||||
confirmCurrentFrame(2);
|
||||
|
||||
// second attempt should progress to input time
|
||||
setTime(2200, 2200);
|
||||
confirmCurrentFrame(2);
|
||||
|
||||
// entering important section
|
||||
setTime(3000, 3000);
|
||||
confirmCurrentFrame(3);
|
||||
|
||||
// cannot progress within
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(3);
|
||||
|
||||
setTime(4000, 4000);
|
||||
confirmCurrentFrame(4);
|
||||
|
||||
// still cannot progress
|
||||
setTime(4500, null);
|
||||
confirmCurrentFrame(4);
|
||||
|
||||
setTime(5200, 5000);
|
||||
confirmCurrentFrame(5);
|
||||
|
||||
// important section AllowedImportantTimeSpan allowance
|
||||
setTime(5200, 5200);
|
||||
confirmCurrentFrame(5);
|
||||
|
||||
setTime(7200, 7000);
|
||||
confirmCurrentFrame(6);
|
||||
|
||||
setTime(7200, null);
|
||||
confirmCurrentFrame(6);
|
||||
|
||||
// exited important section
|
||||
setTime(8200, 8000);
|
||||
confirmCurrentFrame(7);
|
||||
confirmNextFrame(null);
|
||||
|
||||
setTime(8200, null);
|
||||
confirmCurrentFrame(7);
|
||||
confirmNextFrame(null);
|
||||
|
||||
setTime(8400, null);
|
||||
confirmCurrentFrame(7);
|
||||
confirmNextFrame(null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestIntroTime()
|
||||
{
|
||||
setTime(-1000, -1000);
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
|
||||
setTime(-500, -500);
|
||||
confirmCurrentFrame(null);
|
||||
confirmNextFrame(0);
|
||||
|
||||
setTime(0, 0);
|
||||
confirmCurrentFrame(0);
|
||||
confirmNextFrame(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicRewind()
|
||||
{
|
||||
setTime(2800, 0);
|
||||
setTime(2800, 1000);
|
||||
setTime(2800, 2000);
|
||||
setTime(2800, 2800);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(3);
|
||||
|
||||
// pivot without crossing a frame boundary
|
||||
setTime(2700, 2700);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(1);
|
||||
|
||||
// cross current frame boundary; should not yet update frame
|
||||
setTime(1980, 1980);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(1);
|
||||
|
||||
setTime(1200, 1200);
|
||||
confirmCurrentFrame(2);
|
||||
confirmNextFrame(1);
|
||||
|
||||
// ensure each frame plays out until start
|
||||
setTime(-500, 1000);
|
||||
confirmCurrentFrame(1);
|
||||
confirmNextFrame(0);
|
||||
|
||||
setTime(-500, 0);
|
||||
confirmCurrentFrame(0);
|
||||
confirmNextFrame(null);
|
||||
|
||||
setTime(-500, -500);
|
||||
confirmCurrentFrame(0);
|
||||
confirmNextFrame(null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindInsideImportantSection()
|
||||
{
|
||||
fastForwardToPoint(3000);
|
||||
|
||||
setTime(4000, 4000);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(3);
|
||||
|
||||
setTime(3000, 3000);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(2);
|
||||
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(4000, 4000);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(4500, null);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(4000, null);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(5);
|
||||
|
||||
setTime(3500, null);
|
||||
confirmCurrentFrame(4);
|
||||
confirmNextFrame(3);
|
||||
|
||||
setTime(3000, 3000);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRewindOutOfImportantSection()
|
||||
{
|
||||
fastForwardToPoint(3500);
|
||||
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3200, null);
|
||||
// next frame doesn't change even though direction reversed, because of important section.
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(3000, null);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(4);
|
||||
|
||||
setTime(2800, 2800);
|
||||
confirmCurrentFrame(3);
|
||||
confirmNextFrame(2);
|
||||
}
|
||||
|
||||
private void fastForwardToPoint(double destination)
|
||||
{
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
if (handler.SetFrameFromTime(destination) == null)
|
||||
return;
|
||||
}
|
||||
|
||||
throw new TimeoutException("Seek was never fulfilled");
|
||||
}
|
||||
|
||||
private void setTime(double set, double? expect)
|
||||
{
|
||||
Assert.AreEqual(expect, handler.SetFrameFromTime(set));
|
||||
}
|
||||
|
||||
private void confirmCurrentFrame(int? frame)
|
||||
{
|
||||
if (frame.HasValue)
|
||||
{
|
||||
Assert.IsNotNull(handler.CurrentFrame);
|
||||
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(handler.CurrentFrame);
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmNextFrame(int? frame)
|
||||
{
|
||||
if (frame.HasValue)
|
||||
{
|
||||
Assert.IsNotNull(handler.NextFrame);
|
||||
Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(handler.NextFrame);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestReplayFrame : ReplayFrame
|
||||
{
|
||||
public readonly bool IsImportant;
|
||||
|
||||
public TestReplayFrame(double time, bool isImportant = false)
|
||||
: base(time)
|
||||
{
|
||||
IsImportant = isImportant;
|
||||
}
|
||||
}
|
||||
|
||||
private class TestInputHandler : FramedReplayInputHandler<TestReplayFrame>
|
||||
{
|
||||
public TestInputHandler(Replay replay)
|
||||
: base(replay)
|
||||
{
|
||||
FrameAccuratePlayback = true;
|
||||
}
|
||||
|
||||
protected override double AllowedImportantTimeSpan => 1000;
|
||||
|
||||
protected override bool IsImportant(TestReplayFrame frame) => frame.IsImportant;
|
||||
}
|
||||
}
|
||||
}
|
@ -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")]
|
||||
|
@ -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")]
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
219
osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs
Normal file
219
osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -53,13 +53,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
new AudioVisualiser(),
|
||||
}
|
||||
},
|
||||
TimelineArea = new TimelineArea
|
||||
TimelineArea = new TimelineArea(CreateTestComponent())
|
||||
{
|
||||
Child = CreateTestComponent(),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Size = new Vector2(0.8f, 100),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
waitForPlayer();
|
||||
AddAssert("ensure frames arrived", () => replayHandler.HasFrames);
|
||||
|
||||
AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null);
|
||||
AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame);
|
||||
checkPaused(true);
|
||||
|
||||
double? pausedTime = null;
|
||||
@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
sendFrames();
|
||||
|
||||
AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null);
|
||||
AddUntilStep("wait for frame starvation", () => replayHandler.WaitingForFrame);
|
||||
checkPaused(true);
|
||||
|
||||
AddAssert("time advanced", () => currentFrameStableTime > pausedTime);
|
||||
@ -288,7 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
public override void WatchUser(int userId)
|
||||
{
|
||||
if (sentState)
|
||||
if (!PlayingUsers.Contains(userId) && sentState)
|
||||
{
|
||||
// usually the server would do this.
|
||||
sendState(beatmapId);
|
||||
|
@ -204,27 +204,27 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
return;
|
||||
}
|
||||
|
||||
if (replayHandler.NextFrame != null)
|
||||
{
|
||||
var lastFrame = replay.Frames.LastOrDefault();
|
||||
if (!replayHandler.HasFrames)
|
||||
return;
|
||||
|
||||
// this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved).
|
||||
// in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation.
|
||||
if (lastFrame != null)
|
||||
latency = Math.Max(latency, Time.Current - lastFrame.Time);
|
||||
var lastFrame = replay.Frames.LastOrDefault();
|
||||
|
||||
latencyDisplay.Text = $"latency: {latency:N1}";
|
||||
// this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved).
|
||||
// in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation.
|
||||
if (lastFrame != null)
|
||||
latency = Math.Max(latency, Time.Current - lastFrame.Time);
|
||||
|
||||
double proposedTime = Time.Current - latency + Time.Elapsed;
|
||||
latencyDisplay.Text = $"latency: {latency:N1}";
|
||||
|
||||
// this will either advance by one or zero frames.
|
||||
double? time = replayHandler.SetFrameFromTime(proposedTime);
|
||||
double proposedTime = Time.Current - latency + Time.Elapsed;
|
||||
|
||||
if (time == null)
|
||||
return;
|
||||
// this will either advance by one or zero frames.
|
||||
double? time = replayHandler.SetFrameFromTime(proposedTime);
|
||||
|
||||
manualClock.CurrentTime = time.Value;
|
||||
}
|
||||
if (time == null)
|
||||
return;
|
||||
|
||||
manualClock.CurrentTime = time.Value;
|
||||
}
|
||||
|
||||
[TearDownSteps]
|
||||
|
@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")]
|
||||
|
@ -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>
|
||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.GreenDark;
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier at this control point.
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
private const double default_beat_length = 60000.0 / 60.0;
|
||||
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.YellowDark;
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Orange1;
|
||||
|
||||
public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
|
||||
{
|
||||
|
@ -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"/>.
|
||||
|
@ -9,6 +9,9 @@ namespace osu.Game.Extensions
|
||||
{
|
||||
public static class DrawableExtensions
|
||||
{
|
||||
public const double REPEAT_INTERVAL = 70;
|
||||
public const double INITIAL_DELAY = 250;
|
||||
|
||||
/// <summary>
|
||||
/// Helper method that is used while <see cref="IKeyBindingHandler"/> doesn't support repetitions of <see cref="IKeyBindingHandler{T}.OnPressed"/>.
|
||||
/// Simulates repetitions by continually invoking a delegate according to the default key repeat rate.
|
||||
@ -19,12 +22,13 @@ namespace osu.Game.Extensions
|
||||
/// <param name="handler">The <see cref="IKeyBindingHandler{T}"/> which is handling the repeat.</param>
|
||||
/// <param name="scheduler">The <see cref="Scheduler"/> to schedule repetitions on.</param>
|
||||
/// <param name="action">The <see cref="Action"/> to be invoked once immediately and with every repetition.</param>
|
||||
/// <param name="initialRepeatDelay">The delay imposed on the first repeat. Defaults to <see cref="INITIAL_DELAY"/>.</param>
|
||||
/// <returns>A <see cref="ScheduledDelegate"/> which can be cancelled to stop the repeat events from firing.</returns>
|
||||
public static ScheduledDelegate BeginKeyRepeat(this IKeyBindingHandler handler, Scheduler scheduler, Action action)
|
||||
public static ScheduledDelegate BeginKeyRepeat(this IKeyBindingHandler handler, Scheduler scheduler, Action action, double initialRepeatDelay = INITIAL_DELAY)
|
||||
{
|
||||
action();
|
||||
|
||||
ScheduledDelegate repeatDelegate = new ScheduledDelegate(action, handler.Time.Current + 250, 70);
|
||||
ScheduledDelegate repeatDelegate = new ScheduledDelegate(action, handler.Time.Current + initialRepeatDelay, REPEAT_INTERVAL);
|
||||
scheduler.Add(repeatDelegate);
|
||||
return repeatDelegate;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -186,6 +186,13 @@ namespace osu.Game.Graphics
|
||||
public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee");
|
||||
public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff");
|
||||
|
||||
// in latest editor design logic, need to figure out where these sit...
|
||||
public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66");
|
||||
public readonly Color4 Orange1 = Color4Extensions.FromHex(@"ffd966");
|
||||
|
||||
// Content Background
|
||||
public readonly Color4 B5 = Color4Extensions.FromHex(@"222a28");
|
||||
|
||||
public readonly Color4 RedLighter = Color4Extensions.FromHex(@"ffeded");
|
||||
public readonly Color4 RedLight = Color4Extensions.FromHex(@"ed7787");
|
||||
public readonly Color4 Red = Color4Extensions.FromHex(@"ed1121");
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -32,8 +32,6 @@ namespace osu.Game.Input.Handlers
|
||||
|
||||
public override bool Initialize(GameHost host) => true;
|
||||
|
||||
public override bool IsActive => true;
|
||||
|
||||
public class ReplayState<T> : IInput
|
||||
where T : struct
|
||||
{
|
||||
|
506
osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs
generated
Normal file
506
osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -60,6 +60,7 @@ namespace osu.Game.Online.Spectator
|
||||
private IBindable<IReadOnlyList<Mod>> currentMods { get; set; }
|
||||
|
||||
private readonly SpectatorState currentState = new SpectatorState();
|
||||
private readonly Dictionary<int, SpectatorState> currentUserStates = new Dictionary<int, SpectatorState>();
|
||||
|
||||
private bool isPlaying;
|
||||
|
||||
@ -69,7 +70,7 @@ namespace osu.Game.Online.Spectator
|
||||
public event Action<int, FrameDataBundle> OnNewFrames;
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever a user starts a play session.
|
||||
/// Called whenever a user starts a play session, or immediately if the user is being watched and currently in a play session.
|
||||
/// </summary>
|
||||
public event Action<int, SpectatorState> OnUserBeganPlaying;
|
||||
|
||||
@ -134,6 +135,9 @@ namespace osu.Game.Online.Spectator
|
||||
if (!playingUsers.Contains(userId))
|
||||
playingUsers.Add(userId);
|
||||
|
||||
lock (userLock)
|
||||
currentUserStates[userId] = state;
|
||||
|
||||
OnUserBeganPlaying?.Invoke(userId, state);
|
||||
|
||||
return Task.CompletedTask;
|
||||
@ -143,6 +147,9 @@ namespace osu.Game.Online.Spectator
|
||||
{
|
||||
playingUsers.Remove(userId);
|
||||
|
||||
lock (userLock)
|
||||
currentUserStates.Remove(userId);
|
||||
|
||||
OnUserFinishedPlaying?.Invoke(userId, state);
|
||||
|
||||
return Task.CompletedTask;
|
||||
@ -268,5 +275,24 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
lastSendTime = Time.Current;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bind an action to <see cref="OnUserBeganPlaying"/> with the option of running the bound action once immediately.
|
||||
/// </summary>
|
||||
/// <param name="callback">The action to perform when a user begins playing.</param>
|
||||
/// <param name="runOnceImmediately">Whether the action provided in <paramref name="callback"/> should be run once immediately for all users currently playing.</param>
|
||||
public void BindUserBeganPlaying(Action<int, SpectatorState> callback, bool runOnceImmediately = false)
|
||||
{
|
||||
OnUserBeganPlaying += callback;
|
||||
|
||||
if (!runOnceImmediately)
|
||||
return;
|
||||
|
||||
lock (userLock)
|
||||
{
|
||||
foreach (var (userId, state) in currentUserStates)
|
||||
callback(userId, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
||||
|
@ -6,6 +6,8 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Input.Bindings;
|
||||
|
||||
namespace osu.Game.Overlays.Volume
|
||||
@ -15,8 +17,30 @@ namespace osu.Game.Overlays.Volume
|
||||
public Func<GlobalAction, bool> ActionRequested;
|
||||
public Func<GlobalAction, float, bool, bool> ScrollActionRequested;
|
||||
|
||||
public bool OnPressed(GlobalAction action) =>
|
||||
ActionRequested?.Invoke(action) ?? false;
|
||||
private ScheduledDelegate keyRepeat;
|
||||
|
||||
public bool OnPressed(GlobalAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case GlobalAction.DecreaseVolume:
|
||||
case GlobalAction.IncreaseVolume:
|
||||
keyRepeat?.Cancel();
|
||||
keyRepeat = this.BeginKeyRepeat(Scheduler, () => ActionRequested?.Invoke(action), 150);
|
||||
return true;
|
||||
|
||||
case GlobalAction.ToggleMute:
|
||||
ActionRequested?.Invoke(action);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnReleased(GlobalAction action)
|
||||
{
|
||||
keyRepeat?.Cancel();
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
{
|
||||
@ -27,9 +51,5 @@ namespace osu.Game.Overlays.Volume
|
||||
|
||||
public bool OnScroll(GlobalAction action, float amount, bool isPrecise) =>
|
||||
ScrollActionRequested?.Invoke(action, amount, isPrecise) ?? false;
|
||||
|
||||
public void OnReleased(GlobalAction action)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
osu.Game/Rulesets/Edit/BeatmapVerifier.cs
Normal file
24
osu.Game/Rulesets/Edit/BeatmapVerifier.cs
Normal 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));
|
||||
}
|
||||
}
|
61
osu.Game/Rulesets/Edit/Checks/CheckBackground.cs
Normal file
61
osu.Game/Rulesets/Edit/Checks/CheckBackground.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
61
osu.Game/Rulesets/Edit/Checks/Components/CheckCategory.cs
Normal file
61
osu.Game/Rulesets/Edit/Checks/Components/CheckCategory.cs
Normal 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
|
||||
}
|
||||
}
|
24
osu.Game/Rulesets/Edit/Checks/Components/CheckMetadata.cs
Normal file
24
osu.Game/Rulesets/Edit/Checks/Components/CheckMetadata.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
30
osu.Game/Rulesets/Edit/Checks/Components/ICheck.cs
Normal file
30
osu.Game/Rulesets/Edit/Checks/Components/ICheck.cs
Normal 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);
|
||||
}
|
||||
}
|
77
osu.Game/Rulesets/Edit/Checks/Components/Issue.cs
Normal file
77
osu.Game/Rulesets/Edit/Checks/Components/Issue.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
74
osu.Game/Rulesets/Edit/Checks/Components/IssueTemplate.cs
Normal file
74
osu.Game/Rulesets/Edit/Checks/Components/IssueTemplate.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
osu.Game/Rulesets/Edit/Checks/Components/IssueType.cs
Normal file
25
osu.Game/Rulesets/Edit/Checks/Components/IssueType.cs
Normal 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,
|
||||
}
|
||||
}
|
17
osu.Game/Rulesets/Edit/IBeatmapVerifier.cs
Normal file
17
osu.Game/Rulesets/Edit/IBeatmapVerifier.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -49,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"/>.
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Replays;
|
||||
@ -17,80 +16,92 @@ namespace osu.Game.Rulesets.Replays
|
||||
public abstract class FramedReplayInputHandler<TFrame> : ReplayInputHandler
|
||||
where TFrame : ReplayFrame
|
||||
{
|
||||
private readonly Replay replay;
|
||||
/// <summary>
|
||||
/// Whether we have at least one replay frame.
|
||||
/// </summary>
|
||||
public bool HasFrames => Frames.Count != 0;
|
||||
|
||||
protected List<ReplayFrame> Frames => replay.Frames;
|
||||
/// <summary>
|
||||
/// Whether we are waiting for new frames to be received.
|
||||
/// </summary>
|
||||
public bool WaitingForFrame => !replay.HasReceivedAllFrames && currentFrameIndex == Frames.Count - 1;
|
||||
|
||||
/// <summary>
|
||||
/// The current frame of the replay.
|
||||
/// The current time is always between the start and the end time of the current frame.
|
||||
/// </summary>
|
||||
/// <remarks>Returns null if the current time is strictly before the first frame.</remarks>
|
||||
/// <exception cref="InvalidOperationException">The replay is empty.</exception>
|
||||
public TFrame CurrentFrame
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasFrames || !currentFrameIndex.HasValue)
|
||||
return null;
|
||||
if (!HasFrames)
|
||||
throw new InvalidOperationException($"Attempted to get {nameof(CurrentFrame)} of an empty replay");
|
||||
|
||||
return (TFrame)Frames[currentFrameIndex.Value];
|
||||
return currentFrameIndex == -1 ? null : (TFrame)Frames[currentFrameIndex];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The next frame of the replay.
|
||||
/// The start time is always greater or equal to the start time of <see cref="CurrentFrame"/> regardless of the seeking direction.
|
||||
/// </summary>
|
||||
/// <remarks>Returns null if the current frame is the last frame.</remarks>
|
||||
/// <exception cref="InvalidOperationException">The replay is empty.</exception>
|
||||
public TFrame NextFrame
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasFrames)
|
||||
return null;
|
||||
throw new InvalidOperationException($"Attempted to get {nameof(NextFrame)} of an empty replay");
|
||||
|
||||
if (!currentFrameIndex.HasValue)
|
||||
return currentDirection > 0 ? (TFrame)Frames[0] : null;
|
||||
|
||||
int nextFrame = clampedNextFrameIndex;
|
||||
|
||||
if (nextFrame == currentFrameIndex.Value)
|
||||
return null;
|
||||
|
||||
return (TFrame)Frames[clampedNextFrameIndex];
|
||||
return currentFrameIndex == Frames.Count - 1 ? null : (TFrame)Frames[currentFrameIndex + 1];
|
||||
}
|
||||
}
|
||||
|
||||
private int? currentFrameIndex;
|
||||
|
||||
private int clampedNextFrameIndex =>
|
||||
currentFrameIndex.HasValue ? Math.Clamp(currentFrameIndex.Value + currentDirection, 0, Frames.Count - 1) : 0;
|
||||
|
||||
protected FramedReplayInputHandler(Replay replay)
|
||||
{
|
||||
this.replay = replay;
|
||||
}
|
||||
|
||||
private const double sixty_frame_time = 1000.0 / 60;
|
||||
|
||||
protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;
|
||||
|
||||
protected double? CurrentTime { get; private set; }
|
||||
|
||||
private int currentDirection = 1;
|
||||
|
||||
/// <summary>
|
||||
/// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data.
|
||||
/// Disabling this can make replay playback smoother (useful for autoplay, currently).
|
||||
/// </summary>
|
||||
public bool FrameAccuratePlayback;
|
||||
|
||||
public bool HasFrames => Frames.Count > 0;
|
||||
// This input handler should be enabled only if there is at least one replay frame.
|
||||
public override bool IsActive => HasFrames;
|
||||
|
||||
// Can make it non-null but that is a breaking change.
|
||||
protected double? CurrentTime { get; private set; }
|
||||
|
||||
protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;
|
||||
|
||||
protected List<ReplayFrame> Frames => replay.Frames;
|
||||
|
||||
private readonly Replay replay;
|
||||
|
||||
private int currentFrameIndex;
|
||||
|
||||
private const double sixty_frame_time = 1000.0 / 60;
|
||||
|
||||
protected FramedReplayInputHandler(Replay replay)
|
||||
{
|
||||
// TODO: This replay frame ordering should be enforced on the Replay type.
|
||||
// Currently, the ordering can be broken if the frames are added after this construction.
|
||||
replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time));
|
||||
|
||||
this.replay = replay;
|
||||
currentFrameIndex = -1;
|
||||
CurrentTime = double.NegativeInfinity;
|
||||
}
|
||||
|
||||
private bool inImportantSection
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasFrames || !FrameAccuratePlayback)
|
||||
if (!HasFrames || !FrameAccuratePlayback || CurrentFrame == null)
|
||||
return false;
|
||||
|
||||
var frame = currentDirection > 0 ? CurrentFrame : NextFrame;
|
||||
|
||||
if (frame == null)
|
||||
return false;
|
||||
|
||||
return IsImportant(frame) && // a button is in a pressed state
|
||||
Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
|
||||
return IsImportant(CurrentFrame) && // a button is in a pressed state
|
||||
Math.Abs(CurrentTime - NextFrame.Time ?? 0) <= AllowedImportantTimeSpan; // the next frame is within an allowable time span
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,71 +116,52 @@ namespace osu.Game.Rulesets.Replays
|
||||
/// <returns>The usable time value. If null, we should not advance time as we do not have enough data.</returns>
|
||||
public override double? SetFrameFromTime(double time)
|
||||
{
|
||||
updateDirection(time);
|
||||
|
||||
Debug.Assert(currentDirection != 0);
|
||||
|
||||
if (!HasFrames)
|
||||
{
|
||||
// in the case all frames are received, allow time to progress regardless.
|
||||
// In the case all frames are received, allow time to progress regardless.
|
||||
if (replay.HasReceivedAllFrames)
|
||||
return CurrentTime = time;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
TFrame next = NextFrame;
|
||||
double frameStart = getFrameTime(currentFrameIndex);
|
||||
double frameEnd = getFrameTime(currentFrameIndex + 1);
|
||||
|
||||
// if we have a next frame, check if it is before or at the current time in playback, and advance time to it if so.
|
||||
if (next != null)
|
||||
// If the proposed time is after the current frame end time, we progress forwards to precisely the new frame's time (regardless of incoming time).
|
||||
if (frameEnd <= time)
|
||||
{
|
||||
int compare = time.CompareTo(next.Time);
|
||||
|
||||
if (compare == 0 || compare == currentDirection)
|
||||
{
|
||||
currentFrameIndex = clampedNextFrameIndex;
|
||||
return CurrentTime = CurrentFrame.Time;
|
||||
}
|
||||
time = frameEnd;
|
||||
currentFrameIndex++;
|
||||
}
|
||||
// If the proposed time is before the current frame start time, and we are at the frame boundary, we progress backwards.
|
||||
else if (time < frameStart && CurrentTime == frameStart)
|
||||
currentFrameIndex--;
|
||||
|
||||
// at this point, the frame index can't be advanced.
|
||||
// even so, we may be able to propose the clock progresses forward due to being at an extent of the replay,
|
||||
// or moving towards the next valid frame (ie. interpolating in a non-important section).
|
||||
frameStart = getFrameTime(currentFrameIndex);
|
||||
frameEnd = getFrameTime(currentFrameIndex + 1);
|
||||
|
||||
// the exception is if currently in an important section, which is respected above all.
|
||||
if (inImportantSection)
|
||||
// Pause until more frames are arrived.
|
||||
if (WaitingForFrame && frameStart < time)
|
||||
{
|
||||
Debug.Assert(next != null || !replay.HasReceivedAllFrames);
|
||||
CurrentTime = frameStart;
|
||||
return null;
|
||||
}
|
||||
|
||||
// if a next frame does exist, allow interpolation.
|
||||
if (next != null)
|
||||
return CurrentTime = time;
|
||||
CurrentTime = Math.Clamp(time, frameStart, frameEnd);
|
||||
|
||||
// if all frames have been received, allow playing beyond extents.
|
||||
if (replay.HasReceivedAllFrames)
|
||||
return CurrentTime = time;
|
||||
|
||||
// if not all frames are received but we are before the first frame, allow playing.
|
||||
if (time < Frames[0].Time)
|
||||
return CurrentTime = time;
|
||||
|
||||
// in the case we have no next frames and haven't received enough frame data, block.
|
||||
return null;
|
||||
// In an important section, a mid-frame time cannot be used and a null is returned instead.
|
||||
return inImportantSection && frameStart < time && time < frameEnd ? null : CurrentTime;
|
||||
}
|
||||
|
||||
private void updateDirection(double time)
|
||||
private double getFrameTime(int index)
|
||||
{
|
||||
if (!CurrentTime.HasValue)
|
||||
{
|
||||
currentDirection = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentDirection = time.CompareTo(CurrentTime);
|
||||
if (currentDirection == 0) currentDirection = 1;
|
||||
}
|
||||
if (index < 0)
|
||||
return double.NegativeInfinity;
|
||||
if (index >= Frames.Count)
|
||||
return double.PositiveInfinity;
|
||||
|
||||
return Frames[index].Time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public int JudgedHits { get; private set; }
|
||||
|
||||
private JudgementResult lastAppliedResult;
|
||||
|
||||
private readonly BindableBool hasCompleted = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
@ -53,12 +55,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
public void ApplyResult(JudgementResult result)
|
||||
{
|
||||
JudgedHits++;
|
||||
lastAppliedResult = result;
|
||||
|
||||
ApplyResultInternal(result);
|
||||
|
||||
NewJudgement?.Invoke(result);
|
||||
|
||||
updateHasCompleted();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -69,8 +70,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
JudgedHits--;
|
||||
|
||||
updateHasCompleted();
|
||||
|
||||
RevertResultInternal(result);
|
||||
}
|
||||
|
||||
@ -134,6 +133,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
private void updateHasCompleted() => hasCompleted.Value = JudgedHits == MaxHits;
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
hasCompleted.Value = JudgedHits == MaxHits && (JudgedHits == 0 || lastAppliedResult.TimeAbsolute < Clock.CurrentTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -10,6 +11,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Framework.Input.StateChanges.Events;
|
||||
using osu.Framework.Input.States;
|
||||
using osu.Game.Configuration;
|
||||
@ -100,6 +102,17 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
#endregion
|
||||
|
||||
// to avoid allocation
|
||||
private readonly List<IInput> emptyInputList = new List<IInput>();
|
||||
|
||||
protected override List<IInput> GetPendingInputs()
|
||||
{
|
||||
if (replayInputHandler?.IsActive == false)
|
||||
return emptyInputList;
|
||||
|
||||
return base.GetPendingInputs();
|
||||
}
|
||||
|
||||
#region Setting application (disables etc.)
|
||||
|
||||
private Bindable<bool> mouseDisabled;
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ 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 MAX_WIDTH = 4;
|
||||
|
||||
@ -21,9 +21,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
|
||||
|
||||
public PointVisualisation()
|
||||
{
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
RelativePositionAxes = Axes.X;
|
||||
RelativePositionAxes = Axes.Both;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
Anchor = Anchor.CentreLeft;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -1,67 +1,27 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class DifficultyPointPiece : CompositeDrawable
|
||||
public class DifficultyPointPiece : TopPointPiece
|
||||
{
|
||||
private readonly DifficultyControlPoint difficultyPoint;
|
||||
|
||||
private OsuSpriteText speedMultiplierText;
|
||||
private readonly BindableNumber<double> speedMultiplier;
|
||||
|
||||
public DifficultyPointPiece(DifficultyControlPoint difficultyPoint)
|
||||
public DifficultyPointPiece(DifficultyControlPoint point)
|
||||
: base(point)
|
||||
{
|
||||
this.difficultyPoint = difficultyPoint;
|
||||
speedMultiplier = difficultyPoint.SpeedMultiplierBindable.GetBoundCopy();
|
||||
speedMultiplier = point.SpeedMultiplierBindable.GetBoundCopy();
|
||||
|
||||
Y = Height;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
Color4 colour = difficultyPoint.GetRepresentingColour(colours);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colour,
|
||||
Width = 2,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colour,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
speedMultiplierText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Default.With(weight: FontWeight.Bold),
|
||||
Colour = Color4.White,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
speedMultiplier.BindValueChanged(multiplier => speedMultiplierText.Text = $"{multiplier.NewValue:n2}x", true);
|
||||
base.LoadComplete();
|
||||
speedMultiplier.BindValueChanged(multiplier => Label.Text = $"{multiplier.NewValue:n2}x", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,7 @@
|
||||
|
||||
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;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@ -23,7 +21,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
private readonly BindableNumber<int> volume;
|
||||
|
||||
private OsuSpriteText text;
|
||||
private Box volumeBox;
|
||||
private Container volumeBox;
|
||||
|
||||
private const int max_volume_height = 22;
|
||||
|
||||
public SamplePointPiece(SampleControlPoint samplePoint)
|
||||
{
|
||||
@ -35,8 +35,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Origin = Anchor.TopLeft;
|
||||
Anchor = Anchor.TopLeft;
|
||||
Margin = new MarginPadding { Vertical = 5 };
|
||||
|
||||
Origin = Anchor.BottomCentre;
|
||||
Anchor = Anchor.BottomCentre;
|
||||
|
||||
AutoSizeAxes = Axes.X;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
@ -45,40 +47,43 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
volumeBox = new Circle
|
||||
{
|
||||
CornerRadius = 5,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Y = -20,
|
||||
Width = 10,
|
||||
Colour = colour,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 20,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = 16,
|
||||
Masking = true,
|
||||
CornerRadius = 8,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
volumeBox = new Box
|
||||
{
|
||||
X = 2,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Colour = ColourInfo.GradientVertical(colour, Color4.Black),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Colour = colour.Lighten(0.2f),
|
||||
Width = 2,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Colour = colour,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding(5),
|
||||
Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
|
||||
Colour = colours.B5,
|
||||
}
|
||||
}
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
X = 2,
|
||||
Y = -5,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Alpha = 0.9f,
|
||||
Rotation = -90,
|
||||
Font = OsuFont.Default.With(weight: FontWeight.SemiBold)
|
||||
}
|
||||
};
|
||||
|
||||
volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true);
|
||||
volume.BindValueChanged(volume => volumeBox.Height = max_volume_height * volume.NewValue / 100f, true);
|
||||
bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
[Cached]
|
||||
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
|
||||
{
|
||||
private readonly Drawable userContent;
|
||||
public readonly Bindable<bool> WaveformVisible = new Bindable<bool>();
|
||||
|
||||
public readonly Bindable<bool> ControlPointsVisible = new Bindable<bool>();
|
||||
@ -56,8 +57,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
private Track track;
|
||||
|
||||
public Timeline()
|
||||
private const float timeline_height = 72;
|
||||
private const float timeline_expanded_height = 156;
|
||||
|
||||
public Timeline(Drawable userContent)
|
||||
{
|
||||
this.userContent = userContent;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
ZoomDuration = 200;
|
||||
ZoomEasing = Easing.OutQuint;
|
||||
ScrollbarVisible = false;
|
||||
@ -69,18 +77,31 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
private TimelineControlPointDisplay controlPoints;
|
||||
|
||||
private Container mainContent;
|
||||
|
||||
private Bindable<float> waveformOpacity;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config)
|
||||
{
|
||||
CentreMarker centreMarker;
|
||||
|
||||
// We don't want the centre marker to scroll
|
||||
AddInternal(centreMarker = new CentreMarker());
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
controlPoints = new TimelineControlPointDisplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = timeline_expanded_height,
|
||||
},
|
||||
mainContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = timeline_height,
|
||||
Depth = float.MaxValue,
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
waveform = new WaveformGraph
|
||||
{
|
||||
@ -90,8 +111,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
MidColour = colours.BlueDark,
|
||||
HighColour = colours.BlueDarker,
|
||||
},
|
||||
centreMarker.CreateProxy(),
|
||||
ticks = new TimelineTickDisplay(),
|
||||
controlPoints = new TimelineControlPointDisplay(),
|
||||
new Box
|
||||
{
|
||||
Name = "zero marker",
|
||||
@ -100,21 +121,43 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
Origin = Anchor.TopCentre,
|
||||
Colour = colours.YellowDarker,
|
||||
},
|
||||
userContent,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// We don't want the centre marker to scroll
|
||||
AddInternal(new CentreMarker { Depth = float.MaxValue });
|
||||
|
||||
waveformOpacity = config.GetBindable<float>(OsuSetting.EditorWaveformOpacity);
|
||||
Beatmap.BindTo(beatmap);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true);
|
||||
|
||||
WaveformVisible.ValueChanged += _ => updateWaveformOpacity();
|
||||
ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
|
||||
TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
|
||||
ControlPointsVisible.BindValueChanged(visible =>
|
||||
{
|
||||
if (visible.NewValue)
|
||||
{
|
||||
this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint);
|
||||
mainContent.MoveToY(36, 200, Easing.OutQuint);
|
||||
|
||||
// delay the fade in else masking looks weird.
|
||||
controlPoints.Delay(180).FadeIn(400, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
controlPoints.FadeOut(200, Easing.OutQuint);
|
||||
|
||||
// likewise, delay the resize until the fade is complete.
|
||||
this.Delay(180).ResizeHeightTo(timeline_height, 200, Easing.OutQuint);
|
||||
mainContent.Delay(180).MoveToY(0, 200, Easing.OutQuint);
|
||||
}
|
||||
}, true);
|
||||
|
||||
Beatmap.BindTo(beatmap);
|
||||
Beatmap.BindValueChanged(b =>
|
||||
{
|
||||
waveform.Waveform = b.NewValue.Waveform;
|
||||
|
@ -12,11 +12,19 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class TimelineArea : Container
|
||||
public class TimelineArea : CompositeDrawable
|
||||
{
|
||||
public readonly Timeline Timeline = new Timeline { RelativeSizeAxes = Axes.Both };
|
||||
public Timeline Timeline;
|
||||
|
||||
protected override Container<Drawable> Content => Timeline;
|
||||
private readonly Drawable userContent;
|
||||
|
||||
public TimelineArea(Drawable content = null)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
userContent = content ?? Drawable.Empty();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -37,7 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
@ -55,11 +64,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 160,
|
||||
Padding = new MarginPadding { Horizontal = 10 },
|
||||
Padding = new MarginPadding(10),
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 4),
|
||||
Children = new[]
|
||||
@ -123,14 +130,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
}
|
||||
},
|
||||
Timeline
|
||||
Timeline = new Timeline(userContent),
|
||||
},
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Distributed),
|
||||
new Dimension(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -63,7 +63,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
AddInternal(backgroundBox = new SelectableAreaBackground
|
||||
{
|
||||
Colour = Color4.Black
|
||||
Colour = Color4.Black,
|
||||
Depth = float.MaxValue,
|
||||
Blending = BlendingParameters.Additive,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,6 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
|
||||
|
||||
@ -17,11 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
|
||||
|
||||
public TimelineControlPointDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override void LoadBeatmap(EditorBeatmap beatmap)
|
||||
{
|
||||
base.LoadBeatmap(beatmap);
|
||||
|
@ -27,6 +27,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
X = (float)group.Time;
|
||||
}
|
||||
|
||||
|
@ -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,32 @@ 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 Circle 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 Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 5,
|
||||
Colour = Color4.Black.Opacity(0.4f)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,60 +3,27 @@
|
||||
|
||||
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;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class TimingPointPiece : CompositeDrawable
|
||||
public class TimingPointPiece : TopPointPiece
|
||||
{
|
||||
private readonly TimingControlPoint point;
|
||||
|
||||
private readonly BindableNumber<double> beatLength;
|
||||
private OsuSpriteText bpmText;
|
||||
|
||||
public TimingPointPiece(TimingControlPoint point)
|
||||
: base(point)
|
||||
{
|
||||
this.point = point;
|
||||
beatLength = point.BeatLengthBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Origin = Anchor.CentreLeft;
|
||||
Anchor = Anchor.CentreLeft;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Color4 colour = point.GetRepresentingColour(colours);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Alpha = 0.9f,
|
||||
Colour = ColourInfo.GradientHorizontal(colour, colour.Opacity(0.5f)),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
bpmText = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0.9f,
|
||||
Padding = new MarginPadding(3),
|
||||
Font = OsuFont.Default.With(size: 40)
|
||||
}
|
||||
};
|
||||
|
||||
beatLength.BindValueChanged(beatLength =>
|
||||
{
|
||||
bpmText.Text = $"{60000 / beatLength.NewValue:n1} BPM";
|
||||
Label.Text = $"{60000 / beatLength.NewValue:n1} BPM";
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,55 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class TopPointPiece : CompositeDrawable
|
||||
{
|
||||
private readonly ControlPoint point;
|
||||
|
||||
protected OsuSpriteText Label { get; private set; }
|
||||
|
||||
public TopPointPiece(ControlPoint point)
|
||||
{
|
||||
this.point = point;
|
||||
AutoSizeAxes = Axes.X;
|
||||
Height = 16;
|
||||
Margin = new MarginPadding(4);
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = Height / 2;
|
||||
|
||||
Origin = Anchor.TopCentre;
|
||||
Anchor = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = point.GetRepresentingColour(colours),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
Label = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding(3),
|
||||
Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold),
|
||||
Colour = colours.B5,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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 =>
|
||||
|
@ -18,5 +18,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
[Description("timing")]
|
||||
Timing,
|
||||
|
||||
[Description("verify")]
|
||||
Verify,
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,6 @@ namespace osu.Game.Screens.Edit
|
||||
private const float vertical_margins = 10;
|
||||
private const float horizontal_margins = 20;
|
||||
|
||||
private const float timeline_height = 110;
|
||||
|
||||
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
|
||||
|
||||
private Container timelineContainer;
|
||||
@ -40,64 +38,87 @@ namespace osu.Game.Screens.Edit
|
||||
if (beatDivisor != null)
|
||||
this.beatDivisor.BindTo(beatDivisor);
|
||||
|
||||
Children = new Drawable[]
|
||||
Child = new GridContainer
|
||||
{
|
||||
mainContent = new Container
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions = new[]
|
||||
{
|
||||
Name = "Main content",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Horizontal = horizontal_margins,
|
||||
Top = vertical_margins + timeline_height,
|
||||
Bottom = vertical_margins
|
||||
},
|
||||
Child = spinner = new LoadingSpinner(true)
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
},
|
||||
new Container
|
||||
Content = new[]
|
||||
{
|
||||
Name = "Timeline",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = timeline_height,
|
||||
Children = new Drawable[]
|
||||
new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.5f)
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = "Timeline content",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins },
|
||||
Child = new GridContainer
|
||||
Name = "Timeline",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
new Box
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
timelineContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Right = 5 },
|
||||
},
|
||||
new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black.Opacity(0.5f)
|
||||
},
|
||||
ColumnDimensions = new[]
|
||||
new Container
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 90),
|
||||
Name = "Timeline content",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins },
|
||||
Child = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
timelineContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Right = 5 },
|
||||
},
|
||||
new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
|
||||
},
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 90),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
mainContent = new Container
|
||||
{
|
||||
Name = "Main content",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Horizontal = horizontal_margins,
|
||||
Top = vertical_margins,
|
||||
Bottom = vertical_margins
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
Child = spinner = new LoadingSpinner(true)
|
||||
{
|
||||
State = { Value = Visibility.Visible },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -112,14 +133,7 @@ namespace osu.Game.Screens.Edit
|
||||
mainContent.Add(content);
|
||||
content.FadeInFromZero(300, Easing.OutQuint);
|
||||
|
||||
LoadComponentAsync(new TimelineArea
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
CreateTimelineContent(),
|
||||
}
|
||||
}, t =>
|
||||
LoadComponentAsync(new TimelineArea(CreateTimelineContent()), t =>
|
||||
{
|
||||
timelineContainer.Add(t);
|
||||
OnTimelineLoaded(t);
|
||||
|
140
osu.Game/Screens/Edit/EditorTable.cs
Normal file
140
osu.Game/Screens/Edit/EditorTable.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
46
osu.Game/Screens/Edit/Verify/IssueSettings.cs
Normal file
46
osu.Game/Screens/Edit/Verify/IssueSettings.cs
Normal 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[]
|
||||
{
|
||||
};
|
||||
}
|
||||
}
|
128
osu.Game/Screens/Edit/Verify/IssueTable.cs
Normal file
128
osu.Game/Screens/Edit/Verify/IssueTable.cs
Normal 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)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
133
osu.Game/Screens/Edit/Verify/VerifyScreen.cs
Normal file
133
osu.Game/Screens/Edit/Verify/VerifyScreen.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -767,7 +767,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
ApplyToBackground(b =>
|
||||
{
|
||||
b.EnableUserDim.Value = true;
|
||||
b.IgnoreUserSettings.Value = false;
|
||||
b.BlurAmount.Value = 0;
|
||||
|
||||
// bind component bindables.
|
||||
@ -916,7 +916,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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -62,26 +62,42 @@ namespace osu.Game.Screens.Spectate
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
spectatorClient.OnUserBeganPlaying += userBeganPlaying;
|
||||
spectatorClient.OnUserFinishedPlaying += userFinishedPlaying;
|
||||
spectatorClient.OnNewFrames += userSentFrames;
|
||||
|
||||
foreach (var id in UserIds)
|
||||
populateAllUsers().ContinueWith(_ => Schedule(() =>
|
||||
{
|
||||
userLookupCache.GetUserAsync(id).ContinueWith(u => Schedule(() =>
|
||||
spectatorClient.BindUserBeganPlaying(userBeganPlaying, true);
|
||||
spectatorClient.OnUserFinishedPlaying += userFinishedPlaying;
|
||||
spectatorClient.OnNewFrames += userSentFrames;
|
||||
|
||||
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
||||
managerUpdated.BindValueChanged(beatmapUpdated);
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
if (u.Result == null)
|
||||
foreach (var (id, _) in userMap)
|
||||
spectatorClient.WatchUser(id);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private Task populateAllUsers()
|
||||
{
|
||||
var userLookupTasks = new Task[UserIds.Length];
|
||||
|
||||
for (int i = 0; i < UserIds.Length; i++)
|
||||
{
|
||||
var userId = UserIds[i];
|
||||
|
||||
userLookupTasks[i] = userLookupCache.GetUserAsync(userId).ContinueWith(task =>
|
||||
{
|
||||
if (!task.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
lock (stateLock)
|
||||
userMap[id] = u.Result;
|
||||
|
||||
spectatorClient.WatchUser(id);
|
||||
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
userMap[userId] = task.Result;
|
||||
});
|
||||
}
|
||||
|
||||
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
|
||||
managerUpdated.BindValueChanged(beatmapUpdated);
|
||||
return Task.WhenAll(userLookupTasks);
|
||||
}
|
||||
|
||||
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> e)
|
||||
|
18
osu.Game/Utils/BatteryInfo.cs
Normal file
18
osu.Game/Utils/BatteryInfo.cs
Normal 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; }
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user