mirror of
https://github.com/ppy/osu.git
synced 2025-01-27 18:32:56 +08:00
Merge branch 'master' into verify-tab
This commit is contained in:
commit
1ff4e2076f
@ -51,7 +51,7 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
|
<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.413.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -69,7 +69,6 @@ namespace osu.Desktop
|
|||||||
/// Allow a maximum of one unhandled exception, per second of execution.
|
/// Allow a maximum of one unhandled exception, per second of execution.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="arg"></param>
|
/// <param name="arg"></param>
|
||||||
/// <returns></returns>
|
|
||||||
private static bool handleException(Exception arg)
|
private static bool handleException(Exception arg)
|
||||||
{
|
{
|
||||||
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
|
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
|
||||||
|
@ -482,7 +482,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// Retrieves the sample info list at a point in time.
|
/// Retrieves the sample info list at a point in time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to retrieve the sample info list from.</param>
|
/// <param name="time">The time to retrieve the sample info list from.</param>
|
||||||
/// <returns></returns>
|
|
||||||
private IList<HitSampleInfo> sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
|
private IList<HitSampleInfo> sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
|
|
||||||
private void runSpmTest(Mod mod)
|
private void runSpmTest(Mod mod)
|
||||||
{
|
{
|
||||||
SpinnerSpmCounter spmCounter = null;
|
SpinnerSpmCalculator spmCalculator = null;
|
||||||
|
|
||||||
CreateModTest(new ModTestData
|
CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
|
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("fetch SPM counter", () =>
|
AddUntilStep("fetch SPM calculator", () =>
|
||||||
{
|
{
|
||||||
spmCounter = this.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
spmCalculator = this.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||||
return spmCounter != null;
|
return spmCalculator != null;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
|
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
Beatmap = singleSpinnerBeatmap,
|
Beatmap = singleSpinnerBeatmap,
|
||||||
PassCondition = () =>
|
PassCondition = () =>
|
||||||
{
|
{
|
||||||
var counter = Player.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
var counter = Player.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||||
return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
|
return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png
Normal file
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-rpm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -168,13 +168,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
double estimatedSpm = 0;
|
double estimatedSpm = 0;
|
||||||
|
|
||||||
addSeekStep(1000);
|
addSeekStep(1000);
|
||||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
|
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
|
||||||
|
|
||||||
addSeekStep(2000);
|
addSeekStep(2000);
|
||||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
|
||||||
|
|
||||||
addSeekStep(1000);
|
addSeekStep(1000);
|
||||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(0.5)]
|
[TestCase(0.5)]
|
||||||
@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddStep("retrieve spinner state", () =>
|
AddStep("retrieve spinner state", () =>
|
||||||
{
|
{
|
||||||
expectedProgress = drawableSpinner.Progress;
|
expectedProgress = drawableSpinner.Progress;
|
||||||
expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
|
expectedSpm = drawableSpinner.SpinsPerMinute.Value;
|
||||||
});
|
});
|
||||||
|
|
||||||
addSeekStep(0);
|
addSeekStep(0);
|
||||||
@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addSeekStep(1000);
|
addSeekStep(1000);
|
||||||
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
|
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
|
||||||
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
|
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
|
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
|
||||||
|
@ -59,11 +59,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
|||||||
this.slider = slider;
|
this.slider = slider;
|
||||||
ControlPoint = controlPoint;
|
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(_ =>
|
slider.Path.Version.BindValueChanged(_ =>
|
||||||
{
|
{
|
||||||
PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
|
cachePoints(slider);
|
||||||
updatePathType();
|
updatePathType();
|
||||||
}, runOnceImmediately: true);
|
});
|
||||||
|
|
||||||
controlPoint.Type.BindValueChanged(_ => updateMarkerDisplay());
|
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();
|
protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange();
|
||||||
|
|
||||||
|
private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles correction of invalid path types.
|
/// Handles correction of invalid path types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
||||||
|
|
||||||
public SpinnerRotationTracker RotationTracker { get; private set; }
|
public SpinnerRotationTracker RotationTracker { get; private set; }
|
||||||
public SpinnerSpmCounter SpmCounter { get; private set; }
|
|
||||||
|
private SpinnerSpmCalculator spmCalculator;
|
||||||
|
|
||||||
private Container<DrawableSpinnerTick> ticks;
|
private Container<DrawableSpinnerTick> ticks;
|
||||||
private PausableSkinnableSound spinningSample;
|
private PausableSkinnableSound spinningSample;
|
||||||
@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IBindable<double> GainedBonus => gainedBonus;
|
public IBindable<double> GainedBonus => gainedBonus;
|
||||||
|
|
||||||
private readonly Bindable<double> gainedBonus = new Bindable<double>();
|
private readonly Bindable<double> gainedBonus = new BindableDouble();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of spins per minute this spinner is spinning at, for display purposes.
|
||||||
|
/// </summary>
|
||||||
|
public readonly IBindable<double> SpinsPerMinute = new BindableDouble();
|
||||||
|
|
||||||
private const double fade_out_duration = 160;
|
private const double fade_out_duration = 160;
|
||||||
|
|
||||||
@ -63,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
AddRangeInternal(new Drawable[]
|
||||||
{
|
{
|
||||||
|
spmCalculator = new SpinnerSpmCalculator
|
||||||
|
{
|
||||||
|
Result = { BindTarget = SpinsPerMinute },
|
||||||
|
},
|
||||||
ticks = new Container<DrawableSpinnerTick>(),
|
ticks = new Container<DrawableSpinnerTick>(),
|
||||||
new AspectContainer
|
new AspectContainer
|
||||||
{
|
{
|
||||||
@ -77,20 +87,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
RotationTracker = new SpinnerRotationTracker(this)
|
RotationTracker = new SpinnerRotationTracker(this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SpmCounter = new SpinnerSpmCounter
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Y = 120,
|
|
||||||
Alpha = 0
|
|
||||||
},
|
|
||||||
spinningSample = new PausableSkinnableSound
|
spinningSample = new PausableSkinnableSound
|
||||||
{
|
{
|
||||||
Volume = { Value = 0 },
|
Volume = { Value = 0 },
|
||||||
Looping = true,
|
Looping = true,
|
||||||
Frequency = { Value = spinning_sample_initial_frequency }
|
Frequency = { Value = spinning_sample_initial_frequency }
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
|
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
|
||||||
}
|
}
|
||||||
@ -161,17 +164,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateStartTimeStateTransforms()
|
|
||||||
{
|
|
||||||
base.UpdateStartTimeStateTransforms();
|
|
||||||
|
|
||||||
if (Result?.TimeStarted is double startTime)
|
|
||||||
{
|
|
||||||
using (BeginAbsoluteSequence(startTime))
|
|
||||||
fadeInCounter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||||
{
|
{
|
||||||
base.UpdateHitStateTransforms(state);
|
base.UpdateHitStateTransforms(state);
|
||||||
@ -282,22 +274,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
base.UpdateAfterChildren();
|
base.UpdateAfterChildren();
|
||||||
|
|
||||||
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
|
if (Result.TimeStarted == null && RotationTracker.Tracking)
|
||||||
{
|
Result.TimeStarted = Time.Current;
|
||||||
Result.TimeStarted ??= Time.Current;
|
|
||||||
fadeInCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't update after end time to avoid the rate display dropping during fade out.
|
// don't update after end time to avoid the rate display dropping during fade out.
|
||||||
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
|
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
|
||||||
if (Time.Current <= HitObject.EndTime)
|
if (Time.Current <= HitObject.EndTime)
|
||||||
SpmCounter.SetRotation(Result.RateAdjustedRotation);
|
spmCalculator.SetRotation(Result.RateAdjustedRotation);
|
||||||
|
|
||||||
updateBonusScore();
|
updateBonusScore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
|
|
||||||
|
|
||||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||||
|
|
||||||
private int wholeSpins;
|
private int wholeSpins;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
private OsuSpriteText bonusCounter;
|
private OsuSpriteText bonusCounter;
|
||||||
|
|
||||||
|
private Container spmContainer;
|
||||||
|
private OsuSpriteText spmCounter;
|
||||||
|
|
||||||
public DefaultSpinner()
|
public DefaultSpinner()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Font = OsuFont.Numeric.With(size: 24),
|
Font = OsuFont.Numeric.With(size: 24),
|
||||||
Y = -120,
|
Y = -120,
|
||||||
|
},
|
||||||
|
spmContainer = new Container
|
||||||
|
{
|
||||||
|
Alpha = 0f,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Y = 120,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
spmCounter = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Text = @"0",
|
||||||
|
Font = OsuFont.Numeric.With(size: 24)
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Text = @"SPINS PER MINUTE",
|
||||||
|
Font = OsuFont.Numeric.With(size: 12),
|
||||||
|
Y = 30
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private IBindable<double> gainedBonus;
|
private IBindable<double> gainedBonus;
|
||||||
|
private IBindable<double> spinsPerMinute;
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
bonusCounter.FadeOutFromOne(1500);
|
bonusCounter.FadeOutFromOne(1500);
|
||||||
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
|
||||||
|
spinsPerMinute.BindValueChanged(spm =>
|
||||||
|
{
|
||||||
|
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||||
|
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
|
||||||
|
fadeCounterOnTimeStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||||
|
{
|
||||||
|
if (!(drawableHitObject is DrawableSpinner))
|
||||||
|
return;
|
||||||
|
|
||||||
|
fadeCounterOnTimeStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fadeCounterOnTimeStart()
|
||||||
|
{
|
||||||
|
if (drawableSpinner.Result?.TimeStarted is double startTime)
|
||||||
|
{
|
||||||
|
using (BeginAbsoluteSequence(startTime))
|
||||||
|
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,77 +1,37 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
{
|
{
|
||||||
public class SpinnerSpmCounter : Container
|
public class SpinnerSpmCalculator : Component
|
||||||
{
|
{
|
||||||
|
private readonly Queue<RotationRecord> records = new Queue<RotationRecord>();
|
||||||
|
private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The resultant spins per minute value, which is updated via <see cref="SetRotation"/>.
|
||||||
|
/// </summary>
|
||||||
|
public IBindable<double> Result => result;
|
||||||
|
|
||||||
|
private readonly Bindable<double> result = new BindableDouble();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private DrawableHitObject drawableSpinner { get; set; }
|
private DrawableHitObject drawableSpinner { get; set; }
|
||||||
|
|
||||||
private readonly OsuSpriteText spmText;
|
|
||||||
|
|
||||||
public SpinnerSpmCounter()
|
|
||||||
{
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
spmText = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
Text = @"0",
|
|
||||||
Font = OsuFont.Numeric.With(size: 24)
|
|
||||||
},
|
|
||||||
new OsuSpriteText
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
Text = @"SPINS PER MINUTE",
|
|
||||||
Font = OsuFont.Numeric.With(size: 12),
|
|
||||||
Y = 30
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
drawableSpinner.HitObjectApplied += resetState;
|
drawableSpinner.HitObjectApplied += resetState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double spm;
|
|
||||||
|
|
||||||
public double SpinsPerMinute
|
|
||||||
{
|
|
||||||
get => spm;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (value == spm) return;
|
|
||||||
|
|
||||||
spm = value;
|
|
||||||
spmText.Text = Math.Truncate(value).ToString(@"#0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct RotationRecord
|
|
||||||
{
|
|
||||||
public float Rotation;
|
|
||||||
public double Time;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Queue<RotationRecord> records = new Queue<RotationRecord>();
|
|
||||||
private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
|
|
||||||
|
|
||||||
public void SetRotation(float currentRotation)
|
public void SetRotation(float currentRotation)
|
||||||
{
|
{
|
||||||
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
|
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
|
||||||
@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
|
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
|
||||||
record = records.Dequeue();
|
record = records.Dequeue();
|
||||||
|
|
||||||
SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
|
result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
|
||||||
}
|
}
|
||||||
|
|
||||||
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
|
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
|
||||||
@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
private void resetState(DrawableHitObject hitObject)
|
private void resetState(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
SpinsPerMinute = 0;
|
result.Value = 0;
|
||||||
records.Clear();
|
records.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
if (drawableSpinner != null)
|
if (drawableSpinner != null)
|
||||||
drawableSpinner.HitObjectApplied -= resetState;
|
drawableSpinner.HitObjectApplied -= resetState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct RotationRecord
|
||||||
|
{
|
||||||
|
public float Rotation;
|
||||||
|
public double Time;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
protected const float SPRITE_SCALE = 0.625f;
|
protected const float SPRITE_SCALE = 0.625f;
|
||||||
|
|
||||||
|
private const float spm_hide_offset = 50f;
|
||||||
|
|
||||||
protected DrawableSpinner DrawableSpinner { get; private set; }
|
protected DrawableSpinner DrawableSpinner { get; private set; }
|
||||||
|
|
||||||
private Sprite spin;
|
private Sprite spin;
|
||||||
@ -35,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
private LegacySpriteText bonusCounter;
|
private LegacySpriteText bonusCounter;
|
||||||
|
|
||||||
|
private Sprite spmBackground;
|
||||||
|
private LegacySpriteText spmCounter;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
|
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
|
||||||
{
|
{
|
||||||
@ -79,11 +84,27 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
Scale = new Vector2(SPRITE_SCALE),
|
Scale = new Vector2(SPRITE_SCALE),
|
||||||
Y = SPINNER_TOP_OFFSET + 299,
|
Y = SPINNER_TOP_OFFSET + 299,
|
||||||
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
|
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
|
||||||
|
spmBackground = new Sprite
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopLeft,
|
||||||
|
Texture = source.GetTexture("spinner-rpm"),
|
||||||
|
Scale = new Vector2(SPRITE_SCALE),
|
||||||
|
Position = new Vector2(-87, 445 + spm_hide_offset),
|
||||||
|
},
|
||||||
|
spmCounter = new LegacySpriteText(source, LegacyFont.Score)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Scale = new Vector2(SPRITE_SCALE * 0.9f),
|
||||||
|
Position = new Vector2(80, 448 + spm_hide_offset),
|
||||||
|
}.With(s => s.Font = s.Font.With(fixedWidth: false)),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private IBindable<double> gainedBonus;
|
private IBindable<double> gainedBonus;
|
||||||
|
private IBindable<double> spinsPerMinute;
|
||||||
|
|
||||||
private readonly Bindable<bool> completed = new Bindable<bool>();
|
private readonly Bindable<bool> completed = new Bindable<bool>();
|
||||||
|
|
||||||
@ -99,6 +120,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
|
bonusCounter.ScaleTo(SPRITE_SCALE * 2f).Then().ScaleTo(SPRITE_SCALE * 1.28f, 800, Easing.Out);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
|
||||||
|
spinsPerMinute.BindValueChanged(spm =>
|
||||||
|
{
|
||||||
|
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||||
|
}, true);
|
||||||
|
|
||||||
completed.BindValueChanged(onCompletedChanged, true);
|
completed.BindValueChanged(onCompletedChanged, true);
|
||||||
|
|
||||||
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
|
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
|
||||||
@ -142,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
switch (drawableHitObject)
|
switch (drawableHitObject)
|
||||||
{
|
{
|
||||||
case DrawableSpinner d:
|
case DrawableSpinner d:
|
||||||
double fadeOutLength = Math.Min(400, d.HitObject.Duration);
|
using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn))
|
||||||
|
{
|
||||||
|
spmBackground.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
|
||||||
|
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
|
||||||
|
}
|
||||||
|
|
||||||
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true))
|
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
|
||||||
spin.FadeOutFromOne(fadeOutLength);
|
|
||||||
|
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
|
||||||
|
spin.FadeOutFromOne(spinFadeOutLength);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DrawableSpinnerTick d:
|
case DrawableSpinnerTick d:
|
||||||
|
36
osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
Normal file
36
osu.Game.Tests/Mods/ModSettingsEqualityComparison.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Mods
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class ModSettingsEqualityComparison
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Test()
|
||||||
|
{
|
||||||
|
var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } };
|
||||||
|
var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
|
||||||
|
var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
|
||||||
|
var apiMod1 = new APIMod(mod1);
|
||||||
|
var apiMod2 = new APIMod(mod2);
|
||||||
|
var apiMod3 = new APIMod(mod3);
|
||||||
|
|
||||||
|
Assert.That(mod1, Is.Not.EqualTo(mod2));
|
||||||
|
Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
|
||||||
|
|
||||||
|
Assert.That(mod2, Is.EqualTo(mod2));
|
||||||
|
Assert.That(apiMod2, Is.EqualTo(apiMod2));
|
||||||
|
|
||||||
|
Assert.That(mod2, Is.EqualTo(mod3));
|
||||||
|
Assert.That(apiMod2, Is.EqualTo(apiMod3));
|
||||||
|
|
||||||
|
Assert.That(mod3, Is.EqualTo(mod2));
|
||||||
|
Assert.That(apiMod3, Is.EqualTo(apiMod2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,10 @@ using osu.Game.Online.API;
|
|||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Online
|
namespace osu.Game.Tests.Online
|
||||||
{
|
{
|
||||||
@ -84,6 +87,36 @@ namespace osu.Game.Tests.Online
|
|||||||
Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11));
|
Assert.That(converted?.OverallDifficulty.Value, Is.EqualTo(11));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeserialiseScoreInfoWithEmptyMods()
|
||||||
|
{
|
||||||
|
var score = new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo };
|
||||||
|
|
||||||
|
var deserialised = JsonConvert.DeserializeObject<ScoreInfo>(JsonConvert.SerializeObject(score));
|
||||||
|
|
||||||
|
if (deserialised != null)
|
||||||
|
deserialised.Ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
|
||||||
|
Assert.That(deserialised?.Mods.Length, Is.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeserialiseScoreInfoWithCustomModSetting()
|
||||||
|
{
|
||||||
|
var score = new ScoreInfo
|
||||||
|
{
|
||||||
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }
|
||||||
|
};
|
||||||
|
|
||||||
|
var deserialised = JsonConvert.DeserializeObject<ScoreInfo>(JsonConvert.SerializeObject(score));
|
||||||
|
|
||||||
|
if (deserialised != null)
|
||||||
|
deserialised.Ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
|
||||||
|
Assert.That(((OsuModDoubleTime)deserialised?.Mods[0])?.SpeedChange.Value, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
private class TestRuleset : Ruleset
|
private class TestRuleset : Ruleset
|
||||||
{
|
{
|
||||||
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
|
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestDisallowZeroDurationObjects()
|
public void TestDisallowZeroDurationObjects()
|
||||||
{
|
{
|
||||||
DragBar dragBar;
|
DragArea dragArea;
|
||||||
|
|
||||||
AddStep("add spinner", () =>
|
AddStep("add spinner", () =>
|
||||||
{
|
{
|
||||||
@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
EditorBeatmap.Add(new Spinner
|
EditorBeatmap.Add(new Spinner
|
||||||
{
|
{
|
||||||
Position = new Vector2(256, 256),
|
Position = new Vector2(256, 256),
|
||||||
StartTime = 150,
|
StartTime = 2700,
|
||||||
Duration = 500
|
Duration = 500
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("hold down drag bar", () =>
|
AddStep("hold down drag bar", () =>
|
||||||
{
|
{
|
||||||
// distinguishes between the actual drag bar and its "underlay shadow".
|
// distinguishes between the actual drag bar and its "underlay shadow".
|
||||||
dragBar = this.ChildrenOfType<DragBar>().Single(bar => bar.HandlePositionalInput);
|
dragArea = this.ChildrenOfType<DragArea>().Single(bar => bar.HandlePositionalInput);
|
||||||
InputManager.MoveMouseTo(dragBar);
|
InputManager.MoveMouseTo(dragArea);
|
||||||
InputManager.PressButton(MouseButton.Left);
|
InputManager.PressButton(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -64,6 +64,13 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
Clock.Seek(2500);
|
||||||
|
}
|
||||||
|
|
||||||
public abstract Drawable CreateTestComponent();
|
public abstract Drawable CreateTestComponent();
|
||||||
|
|
||||||
private class AudioVisualiser : CompositeDrawable
|
private class AudioVisualiser : CompositeDrawable
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
@ -38,6 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
|
private OsuConfigManager config;
|
||||||
|
|
||||||
public TestSceneMultiplayerGameplayLeaderboard()
|
public TestSceneMultiplayerGameplayLeaderboard()
|
||||||
{
|
{
|
||||||
base.Content.Children = new Drawable[]
|
base.Content.Children = new Drawable[]
|
||||||
@ -48,6 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
|
||||||
|
}
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public override void SetUpSteps()
|
public override void SetUpSteps()
|
||||||
{
|
{
|
||||||
@ -97,6 +106,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users);
|
AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeScoringMode()
|
||||||
|
{
|
||||||
|
AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 5);
|
||||||
|
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
|
||||||
|
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
|
||||||
|
}
|
||||||
|
|
||||||
public class TestMultiplayerStreaming : SpectatorStreamingClient
|
public class TestMultiplayerStreaming : SpectatorStreamingClient
|
||||||
{
|
{
|
||||||
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
|
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
|
||||||
@ -163,7 +180,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty<LegacyReplayFrame>()));
|
((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, new[] { new LegacyReplayFrame(Time.Current, 0, 0, ReplayButtonState.None) }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddAssert("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
|
AddUntilStep("match started", () => Client.Room?.State == MultiplayerRoomState.WaitingForLoad);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,229 @@
|
|||||||
|
// 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 System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Online;
|
||||||
|
using osu.Game.Online.Spectator;
|
||||||
|
using osu.Game.Replays.Legacy;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
|
{
|
||||||
|
public class TestSceneMultiplayerSpectatorLeaderboard : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
[Cached(typeof(SpectatorStreamingClient))]
|
||||||
|
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
|
||||||
|
|
||||||
|
[Cached(typeof(UserLookupCache))]
|
||||||
|
private UserLookupCache lookupCache = new TestUserLookupCache();
|
||||||
|
|
||||||
|
protected override Container<Drawable> Content => content;
|
||||||
|
private readonly Container content;
|
||||||
|
|
||||||
|
private readonly Dictionary<int, ManualClock> clocks = new Dictionary<int, ManualClock>
|
||||||
|
{
|
||||||
|
{ 55, new ManualClock() },
|
||||||
|
{ 56, new ManualClock() }
|
||||||
|
};
|
||||||
|
|
||||||
|
public TestSceneMultiplayerSpectatorLeaderboard()
|
||||||
|
{
|
||||||
|
base.Content.AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
streamingClient,
|
||||||
|
lookupCache,
|
||||||
|
content = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public new void SetUpSteps()
|
||||||
|
{
|
||||||
|
MultiplayerSpectatorLeaderboard leaderboard = null;
|
||||||
|
|
||||||
|
AddStep("reset", () =>
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
|
||||||
|
foreach (var (userId, clock) in clocks)
|
||||||
|
{
|
||||||
|
streamingClient.EndPlay(userId, 0);
|
||||||
|
clock.CurrentTime = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("create leaderboard", () =>
|
||||||
|
{
|
||||||
|
foreach (var (userId, _) in clocks)
|
||||||
|
streamingClient.StartPlay(userId, 0);
|
||||||
|
|
||||||
|
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
||||||
|
|
||||||
|
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
||||||
|
var scoreProcessor = new OsuScoreProcessor();
|
||||||
|
scoreProcessor.ApplyBeatmap(playable);
|
||||||
|
|
||||||
|
LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
|
||||||
|
|
||||||
|
AddStep("add clock sources", () =>
|
||||||
|
{
|
||||||
|
foreach (var (userId, clock) in clocks)
|
||||||
|
leaderboard.AddClock(userId, clock);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLeaderboardTracksCurrentTime()
|
||||||
|
{
|
||||||
|
AddStep("send frames", () =>
|
||||||
|
{
|
||||||
|
// For user 55, send frames in sets of 1.
|
||||||
|
// For user 56, send frames in sets of 10.
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
streamingClient.SendFrames(55, i, 1);
|
||||||
|
|
||||||
|
if (i % 10 == 0)
|
||||||
|
streamingClient.SendFrames(56, i, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertCombo(55, 1);
|
||||||
|
assertCombo(56, 10);
|
||||||
|
|
||||||
|
// Advance to a point where only user 55's frame changes.
|
||||||
|
setTime(500);
|
||||||
|
assertCombo(55, 5);
|
||||||
|
assertCombo(56, 10);
|
||||||
|
|
||||||
|
// Advance to a point where both user's frame changes.
|
||||||
|
setTime(1100);
|
||||||
|
assertCombo(55, 11);
|
||||||
|
assertCombo(56, 20);
|
||||||
|
|
||||||
|
// Advance user 56 only to a point where its frame changes.
|
||||||
|
setTime(56, 2100);
|
||||||
|
assertCombo(55, 11);
|
||||||
|
assertCombo(56, 30);
|
||||||
|
|
||||||
|
// Advance both users beyond their last frame
|
||||||
|
setTime(101 * 100);
|
||||||
|
assertCombo(55, 100);
|
||||||
|
assertCombo(56, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoFrames()
|
||||||
|
{
|
||||||
|
assertCombo(55, 0);
|
||||||
|
assertCombo(56, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTime(double time) => AddStep($"set time {time}", () =>
|
||||||
|
{
|
||||||
|
foreach (var (_, clock) in clocks)
|
||||||
|
clock.CurrentTime = time;
|
||||||
|
});
|
||||||
|
|
||||||
|
private void setTime(int userId, double time)
|
||||||
|
=> AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
|
||||||
|
|
||||||
|
private void assertCombo(int userId, int expectedCombo)
|
||||||
|
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
|
||||||
|
|
||||||
|
private class TestSpectatorStreamingClient : SpectatorStreamingClient
|
||||||
|
{
|
||||||
|
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
|
||||||
|
private readonly Dictionary<int, bool> userSentStateDictionary = new Dictionary<int, bool>();
|
||||||
|
|
||||||
|
public TestSpectatorStreamingClient()
|
||||||
|
: base(new DevelopmentEndpointConfiguration())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartPlay(int userId, int beatmapId)
|
||||||
|
{
|
||||||
|
userBeatmapDictionary[userId] = beatmapId;
|
||||||
|
userSentStateDictionary[userId] = false;
|
||||||
|
sendState(userId, beatmapId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndPlay(int userId, int beatmapId)
|
||||||
|
{
|
||||||
|
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
|
||||||
|
{
|
||||||
|
BeatmapID = beatmapId,
|
||||||
|
RulesetID = 0,
|
||||||
|
});
|
||||||
|
userSentStateDictionary[userId] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendFrames(int userId, int index, int count)
|
||||||
|
{
|
||||||
|
var frames = new List<LegacyReplayFrame>();
|
||||||
|
|
||||||
|
for (int i = index; i < index + count; i++)
|
||||||
|
{
|
||||||
|
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
|
||||||
|
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
|
||||||
|
}
|
||||||
|
|
||||||
|
var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
|
||||||
|
((ISpectatorClient)this).UserSentFrames(userId, bundle);
|
||||||
|
if (!userSentStateDictionary[userId])
|
||||||
|
sendState(userId, userBeatmapDictionary[userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WatchUser(int userId)
|
||||||
|
{
|
||||||
|
if (userSentStateDictionary[userId])
|
||||||
|
{
|
||||||
|
// usually the server would do this.
|
||||||
|
sendState(userId, userBeatmapDictionary[userId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.WatchUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendState(int userId, int beatmapId)
|
||||||
|
{
|
||||||
|
((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
|
||||||
|
{
|
||||||
|
BeatmapID = beatmapId,
|
||||||
|
RulesetID = 0,
|
||||||
|
});
|
||||||
|
userSentStateDictionary[userId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestUserLookupCache : UserLookupCache
|
||||||
|
{
|
||||||
|
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new User
|
||||||
|
{
|
||||||
|
Id = lookup,
|
||||||
|
Username = $"User {lookup}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
|
{
|
||||||
|
public class TestSceneMultiplayerSpectatorPlayerGrid : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
private PlayerGrid grid;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Child = grid = new PlayerGrid { RelativeSizeAxes = Axes.Both };
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMaximiseAndMinimise()
|
||||||
|
{
|
||||||
|
addCells(2);
|
||||||
|
|
||||||
|
assertMaximisation(0, false, true);
|
||||||
|
assertMaximisation(1, false, true);
|
||||||
|
|
||||||
|
clickCell(0);
|
||||||
|
assertMaximisation(0, true);
|
||||||
|
assertMaximisation(1, false, true);
|
||||||
|
clickCell(0);
|
||||||
|
assertMaximisation(0, false);
|
||||||
|
assertMaximisation(1, false, true);
|
||||||
|
|
||||||
|
clickCell(1);
|
||||||
|
assertMaximisation(1, true);
|
||||||
|
assertMaximisation(0, false, true);
|
||||||
|
clickCell(1);
|
||||||
|
assertMaximisation(1, false);
|
||||||
|
assertMaximisation(0, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClickBothCellsSimultaneously()
|
||||||
|
{
|
||||||
|
addCells(2);
|
||||||
|
|
||||||
|
AddStep("click cell 0 then 1", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(grid.Content.ElementAt(0));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
|
||||||
|
InputManager.MoveMouseTo(grid.Content.ElementAt(1));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertMaximisation(1, true);
|
||||||
|
assertMaximisation(0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1)]
|
||||||
|
[TestCase(2)]
|
||||||
|
[TestCase(3)]
|
||||||
|
[TestCase(4)]
|
||||||
|
[TestCase(5)]
|
||||||
|
[TestCase(9)]
|
||||||
|
[TestCase(11)]
|
||||||
|
[TestCase(12)]
|
||||||
|
[TestCase(15)]
|
||||||
|
[TestCase(16)]
|
||||||
|
public void TestCellCount(int count)
|
||||||
|
{
|
||||||
|
addCells(count);
|
||||||
|
AddWaitStep("wait for display", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCells(int count) => AddStep($"add {count} grid cells", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
grid.Add(new GridContent());
|
||||||
|
});
|
||||||
|
|
||||||
|
private void clickCell(int index) => AddStep($"click cell index {index}", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(grid.Content.ElementAt(index));
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
private void assertMaximisation(int index, bool shouldBeMaximised, bool instant = false)
|
||||||
|
{
|
||||||
|
string assertionText = $"cell index {index} {(shouldBeMaximised ? "is" : "is not")} maximised";
|
||||||
|
|
||||||
|
if (instant)
|
||||||
|
AddAssert(assertionText, checkAction);
|
||||||
|
else
|
||||||
|
AddUntilStep(assertionText, checkAction);
|
||||||
|
|
||||||
|
bool checkAction() => Precision.AlmostEquals(grid.MaximisedFacade.DrawSize, grid.Content.ElementAt(index).DrawSize, 10) == shouldBeMaximised;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GridContent : Box
|
||||||
|
{
|
||||||
|
public GridContent()
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,13 +19,12 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
UserHistoryGraph graph;
|
UserHistoryGraph graph;
|
||||||
|
|
||||||
Add(graph = new UserHistoryGraph
|
Add(graph = new UserHistoryGraph("Test")
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 200,
|
Height = 200,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
TooltipCounterName = "Test"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var values = new[]
|
var values = new[]
|
||||||
|
@ -141,7 +141,6 @@ namespace osu.Game.Tournament
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add missing player info based on user IDs.
|
/// Add missing player info based on user IDs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
|
||||||
private bool addPlayers()
|
private bool addPlayers()
|
||||||
{
|
{
|
||||||
bool addedInfo = false;
|
bool addedInfo = false;
|
||||||
|
@ -44,7 +44,6 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns statistics for the <see cref="HitObjects"/> contained in this beatmap.
|
/// Returns statistics for the <see cref="HitObjects"/> contained in this beatmap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
|
||||||
IEnumerable<BeatmapStatistic> GetStatistics();
|
IEnumerable<BeatmapStatistic> GetStatistics();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Configuration;
|
using osu.Framework.Configuration;
|
||||||
using osu.Framework.Configuration.Tracking;
|
using osu.Framework.Configuration.Tracking;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
@ -143,7 +142,7 @@ namespace osu.Game.Configuration
|
|||||||
|
|
||||||
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
|
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
|
||||||
|
|
||||||
SetDefault(OsuSetting.EditorWaveformOpacity, 1f);
|
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OsuConfigManager(Storage storage)
|
public OsuConfigManager(Storage storage)
|
||||||
@ -169,14 +168,9 @@ namespace osu.Game.Configuration
|
|||||||
|
|
||||||
int combined = (year * 10000) + monthDay;
|
int combined = (year * 10000) + monthDay;
|
||||||
|
|
||||||
if (combined < 20200305)
|
if (combined < 20210413)
|
||||||
{
|
{
|
||||||
// the maximum value of this setting was changed.
|
SetValue(OsuSetting.EditorWaveformOpacity, 0.25f);
|
||||||
// if we don't manually increase this, it causes song select to filter out beatmaps the user expects to see.
|
|
||||||
var maxStars = (BindableDouble)GetOriginalBindable<double>(OsuSetting.DisplayStarsMaximum);
|
|
||||||
|
|
||||||
if (maxStars.Value == 10)
|
|
||||||
maxStars.Value = maxStars.MaxValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ namespace osu.Game.Configuration
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="rulesetId">The ruleset's internal ID.</param>
|
/// <param name="rulesetId">The ruleset's internal ID.</param>
|
||||||
/// <param name="variant">An optional variant.</param>
|
/// <param name="variant">An optional variant.</param>
|
||||||
/// <returns></returns>
|
|
||||||
public List<DatabasedSetting> Query(int? rulesetId = null, int? variant = null) =>
|
public List<DatabasedSetting> Query(int? rulesetId = null, int? variant = null) =>
|
||||||
ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
||||||
|
|
||||||
|
@ -346,7 +346,6 @@ namespace osu.Game.Graphics.Backgrounds
|
|||||||
/// such that the smaller triangles appear on top.
|
/// such that the smaller triangles appear on top.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="other"></param>
|
/// <param name="other"></param>
|
||||||
/// <returns></returns>
|
|
||||||
public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale);
|
public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ namespace osu.Game.Graphics.Containers
|
|||||||
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
||||||
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
||||||
|
|
||||||
|
protected override bool BlockScrollInput => false;
|
||||||
|
|
||||||
protected override bool BlockNonPositionalInput => true;
|
protected override bool BlockNonPositionalInput => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -22,7 +22,6 @@ namespace osu.Game.IO.Serialization
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the default <see cref="JsonSerializerSettings"/> that should be used for all <see cref="IJsonSerializable"/>s.
|
/// Creates the default <see cref="JsonSerializerSettings"/> that should be used for all <see cref="IJsonSerializable"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
|
||||||
public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings
|
public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||||
|
@ -98,9 +98,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
|
public IEnumerable<KeyBinding> AudioControlKeyBindings => new[]
|
||||||
{
|
{
|
||||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume),
|
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.Down }, GlobalAction.DecreaseVolume),
|
||||||
new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume),
|
|
||||||
|
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute),
|
new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute),
|
||||||
|
|
||||||
|
@ -85,7 +85,6 @@ namespace osu.Game.Input
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="rulesetId">The ruleset's internal ID.</param>
|
/// <param name="rulesetId">The ruleset's internal ID.</param>
|
||||||
/// <param name="variant">An optional variant.</param>
|
/// <param name="variant">An optional variant.</param>
|
||||||
/// <returns></returns>
|
|
||||||
public List<DatabasedKeyBinding> Query(int? rulesetId = null, int? variant = null) =>
|
public List<DatabasedKeyBinding> Query(int? rulesetId = null, int? variant = null) =>
|
||||||
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,11 +11,12 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Online.API
|
namespace osu.Game.Online.API
|
||||||
{
|
{
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
public class APIMod : IMod
|
public class APIMod : IMod, IEquatable<APIMod>
|
||||||
{
|
{
|
||||||
[JsonProperty("acronym")]
|
[JsonProperty("acronym")]
|
||||||
[Key(0)]
|
[Key(0)]
|
||||||
@ -63,7 +64,16 @@ namespace osu.Game.Online.API
|
|||||||
return resultMod;
|
return resultMod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(IMod other) => Acronym == other?.Acronym;
|
public bool Equals(IMod other) => other is APIMod them && Equals(them);
|
||||||
|
|
||||||
|
public bool Equals(APIMod other)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, other)) return false;
|
||||||
|
if (ReferenceEquals(this, other)) return true;
|
||||||
|
|
||||||
|
return Acronym == other.Acronym &&
|
||||||
|
Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
|
||||||
|
}
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
@ -72,5 +82,20 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
return $"{Acronym}";
|
return $"{Acronym}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ModSettingsEqualityComparer : IEqualityComparer<KeyValuePair<string, object>>
|
||||||
|
{
|
||||||
|
public static ModSettingsEqualityComparer Default { get; } = new ModSettingsEqualityComparer();
|
||||||
|
|
||||||
|
public bool Equals(KeyValuePair<string, object> x, KeyValuePair<string, object> y)
|
||||||
|
{
|
||||||
|
object xValue = ModUtils.GetSettingUnderlyingValue(x.Value);
|
||||||
|
object yValue = ModUtils.GetSettingUnderlyingValue(y.Value);
|
||||||
|
|
||||||
|
return x.Key == y.Key && EqualityComparer<object>.Default.Equals(xValue, yValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetHashCode(KeyValuePair<string, object> obj) => HashCode.Combine(obj.Key, ModUtils.GetSettingUnderlyingValue(obj.Value));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,10 @@
|
|||||||
|
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
using MessagePack.Formatters;
|
using MessagePack.Formatters;
|
||||||
using osu.Framework.Bindables;
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Online.API
|
namespace osu.Game.Online.API
|
||||||
{
|
{
|
||||||
@ -24,36 +23,7 @@ namespace osu.Game.Online.API
|
|||||||
var stringBytes = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(kvp.Key));
|
var stringBytes = new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(kvp.Key));
|
||||||
writer.WriteString(in stringBytes);
|
writer.WriteString(in stringBytes);
|
||||||
|
|
||||||
switch (kvp.Value)
|
primitiveFormatter.Serialize(ref writer, ModUtils.GetSettingUnderlyingValue(kvp.Value), options);
|
||||||
{
|
|
||||||
case Bindable<double> d:
|
|
||||||
primitiveFormatter.Serialize(ref writer, d.Value, options);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Bindable<int> i:
|
|
||||||
primitiveFormatter.Serialize(ref writer, i.Value, options);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Bindable<float> f:
|
|
||||||
primitiveFormatter.Serialize(ref writer, f.Value, options);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Bindable<bool> b:
|
|
||||||
primitiveFormatter.Serialize(ref writer, b.Value, options);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case IBindable u:
|
|
||||||
// A mod with unknown (e.g. enum) generic type.
|
|
||||||
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
|
|
||||||
Debug.Assert(valueMethod != null);
|
|
||||||
primitiveFormatter.Serialize(ref writer, valueMethod.GetValue(u), options);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// fall back for non-bindable cases.
|
|
||||||
primitiveFormatter.Serialize(ref writer, kvp.Value, options);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -433,12 +433,15 @@ namespace osu.Game
|
|||||||
if (paths.Length == 0)
|
if (paths.Length == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var extension = Path.GetExtension(paths.First())?.ToLowerInvariant();
|
var filesPerExtension = paths.GroupBy(p => Path.GetExtension(p).ToLowerInvariant());
|
||||||
|
|
||||||
foreach (var importer in fileImporters)
|
foreach (var groups in filesPerExtension)
|
||||||
{
|
{
|
||||||
if (importer.HandledExtensions.Contains(extension))
|
foreach (var importer in fileImporters)
|
||||||
await importer.Import(paths).ConfigureAwait(false);
|
{
|
||||||
|
if (importer.HandledExtensions.Contains(groups.Key))
|
||||||
|
await importer.Import(groups.ToArray()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
{
|
{
|
||||||
private ProfileLineChart chart;
|
private ProfileLineChart chart;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the history graph tooltip.
|
||||||
|
/// </summary>
|
||||||
|
protected abstract string GraphCounterName { get; }
|
||||||
|
|
||||||
protected ChartProfileSubsection(Bindable<User> user, string headerText)
|
protected ChartProfileSubsection(Bindable<User> user, string headerText)
|
||||||
: base(user, headerText)
|
: base(user, headerText)
|
||||||
{
|
{
|
||||||
@ -30,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
Left = 20,
|
Left = 20,
|
||||||
Right = 40
|
Right = 40
|
||||||
},
|
},
|
||||||
Child = chart = new ProfileLineChart()
|
Child = chart = new ProfileLineChart(GraphCounterName)
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
@ -9,6 +9,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
{
|
{
|
||||||
public class PlayHistorySubsection : ChartProfileSubsection
|
public class PlayHistorySubsection : ChartProfileSubsection
|
||||||
{
|
{
|
||||||
|
protected override string GraphCounterName => "Plays";
|
||||||
|
|
||||||
public PlayHistorySubsection(Bindable<User> user)
|
public PlayHistorySubsection(Bindable<User> user)
|
||||||
: base(user, "Play History")
|
: base(user, "Play History")
|
||||||
{
|
{
|
||||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
private readonly Container<TickLine> rowLinesContainer;
|
private readonly Container<TickLine> rowLinesContainer;
|
||||||
private readonly Container<TickLine> columnLinesContainer;
|
private readonly Container<TickLine> columnLinesContainer;
|
||||||
|
|
||||||
public ProfileLineChart()
|
public ProfileLineChart(string graphCounterName)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
Height = 250;
|
Height = 250;
|
||||||
@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
graph = new UserHistoryGraph
|
graph = new UserHistoryGraph(graphCounterName)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
{
|
{
|
||||||
public class ReplaysSubsection : ChartProfileSubsection
|
public class ReplaysSubsection : ChartProfileSubsection
|
||||||
{
|
{
|
||||||
|
protected override string GraphCounterName => "Replays Watched";
|
||||||
|
|
||||||
public ReplaysSubsection(Bindable<User> user)
|
public ReplaysSubsection(Bindable<User> user)
|
||||||
: base(user, "Replays Watched History")
|
: base(user, "Replays Watched History")
|
||||||
{
|
{
|
||||||
|
@ -11,25 +11,28 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
{
|
{
|
||||||
public class UserHistoryGraph : UserGraph<DateTime, long>
|
public class UserHistoryGraph : UserGraph<DateTime, long>
|
||||||
{
|
{
|
||||||
|
private readonly string tooltipCounterName;
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
public UserHistoryCount[] Values
|
public UserHistoryCount[] Values
|
||||||
{
|
{
|
||||||
set => Data = value?.Select(v => new KeyValuePair<DateTime, long>(v.Date, v.Count)).ToArray();
|
set => Data = value?.Select(v => new KeyValuePair<DateTime, long>(v.Date, v.Count)).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public UserHistoryGraph(string tooltipCounterName)
|
||||||
/// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the <see cref="HistoryGraphTooltip"/>.
|
{
|
||||||
/// </summary>
|
this.tooltipCounterName = tooltipCounterName;
|
||||||
public string TooltipCounterName { get; set; } = "Plays";
|
}
|
||||||
|
|
||||||
protected override float GetDataPointHeight(long playCount) => playCount;
|
protected override float GetDataPointHeight(long playCount) => playCount;
|
||||||
|
|
||||||
protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(TooltipCounterName);
|
protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(tooltipCounterName);
|
||||||
|
|
||||||
protected override object GetTooltipContent(DateTime date, long playCount)
|
protected override object GetTooltipContent(DateTime date, long playCount)
|
||||||
{
|
{
|
||||||
return new TooltipDisplayContent
|
return new TooltipDisplayContent
|
||||||
{
|
{
|
||||||
|
Name = tooltipCounterName,
|
||||||
Count = playCount.ToString("N0"),
|
Count = playCount.ToString("N0"),
|
||||||
Date = date.ToString("MMMM yyyy")
|
Date = date.ToString("MMMM yyyy")
|
||||||
};
|
};
|
||||||
@ -37,14 +40,17 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
|
|
||||||
protected class HistoryGraphTooltip : UserGraphTooltip
|
protected class HistoryGraphTooltip : UserGraphTooltip
|
||||||
{
|
{
|
||||||
|
private readonly string tooltipCounterName;
|
||||||
|
|
||||||
public HistoryGraphTooltip(string tooltipCounterName)
|
public HistoryGraphTooltip(string tooltipCounterName)
|
||||||
: base(tooltipCounterName)
|
: base(tooltipCounterName)
|
||||||
{
|
{
|
||||||
|
this.tooltipCounterName = tooltipCounterName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool SetContent(object content)
|
public override bool SetContent(object content)
|
||||||
{
|
{
|
||||||
if (!(content is TooltipDisplayContent info))
|
if (!(content is TooltipDisplayContent info) || info.Name != tooltipCounterName)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
Counter.Text = info.Count;
|
Counter.Text = info.Count;
|
||||||
@ -55,6 +61,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
|||||||
|
|
||||||
private class TooltipDisplayContent
|
private class TooltipDisplayContent
|
||||||
{
|
{
|
||||||
|
public string Name;
|
||||||
public string Count;
|
public string Count;
|
||||||
public string Date;
|
public string Date;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.IO.Serialization;
|
using osu.Game.IO.Serialization;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mods
|
namespace osu.Game.Rulesets.Mods
|
||||||
{
|
{
|
||||||
@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
/// The base class for gameplay modifiers.
|
/// The base class for gameplay modifiers.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ExcludeFromDynamicCompile]
|
[ExcludeFromDynamicCompile]
|
||||||
public abstract class Mod : IMod, IJsonSerializable
|
public abstract class Mod : IMod, IEquatable<Mod>, IJsonSerializable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of this mod.
|
/// The name of this mod.
|
||||||
@ -172,7 +173,19 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
target.Parse(source);
|
target.Parse(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(IMod other) => GetType() == other?.GetType();
|
public bool Equals(IMod other) => other is Mod them && Equals(them);
|
||||||
|
|
||||||
|
public bool Equals(Mod other)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, other)) return false;
|
||||||
|
if (ReferenceEquals(this, other)) return true;
|
||||||
|
|
||||||
|
return GetType() == other.GetType() &&
|
||||||
|
this.GetSettingsSourceProperties().All(pair =>
|
||||||
|
EqualityComparer<object>.Default.Equals(
|
||||||
|
ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(this)),
|
||||||
|
ModUtils.GetSettingUnderlyingValue(pair.Item2.GetValue(other))));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reset all custom settings for this mod back to their defaults.
|
/// Reset all custom settings for this mod back to their defaults.
|
||||||
|
@ -574,7 +574,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// Calculate the position to be used for sample playback at a specified X position (0..1).
|
/// Calculate the position to be used for sample playback at a specified X position (0..1).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="position">The lookup X position. Generally should be <see cref="SamplePlaybackPosition"/>.</param>
|
/// <param name="position">The lookup X position. Generally should be <see cref="SamplePlaybackPosition"/>.</param>
|
||||||
/// <returns></returns>
|
|
||||||
protected double CalculateSamplePlaybackBalance(double position)
|
protected double CalculateSamplePlaybackBalance(double position)
|
||||||
{
|
{
|
||||||
const float balance_adjust_amount = 0.4f;
|
const float balance_adjust_amount = 0.4f;
|
||||||
|
@ -147,7 +147,6 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
/// to 1 (end of the path).
|
/// to 1 (end of the path).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="progress">Ranges from 0 (beginning of the path) to 1 (end of the path).</param>
|
/// <param name="progress">Ranges from 0 (beginning of the path) to 1 (end of the path).</param>
|
||||||
/// <returns></returns>
|
|
||||||
public Vector2 PositionAt(double progress)
|
public Vector2 PositionAt(double progress)
|
||||||
{
|
{
|
||||||
ensureValid();
|
ensureValid();
|
||||||
@ -161,7 +160,6 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
/// The first point has a PathType which all other points inherit.
|
/// The first point has a PathType which all other points inherit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="controlPoint">One of the control points in the segment.</param>
|
/// <param name="controlPoint">One of the control points in the segment.</param>
|
||||||
/// <returns></returns>
|
|
||||||
public List<PathControlPoint> PointsInSegment(PathControlPoint controlPoint)
|
public List<PathControlPoint> PointsInSegment(PathControlPoint controlPoint)
|
||||||
{
|
{
|
||||||
bool found = false;
|
bool found = false;
|
||||||
|
@ -146,7 +146,6 @@ namespace osu.Game.Rulesets
|
|||||||
/// <param name="beatmap">The beatmap to create the hit renderer for.</param>
|
/// <param name="beatmap">The beatmap to create the hit renderer for.</param>
|
||||||
/// <param name="mods">The <see cref="Mod"/>s to apply.</param>
|
/// <param name="mods">The <see cref="Mod"/>s to apply.</param>
|
||||||
/// <exception cref="BeatmapInvalidForRulesetException">Unable to successfully load the beatmap to be usable with this ruleset.</exception>
|
/// <exception cref="BeatmapInvalidForRulesetException">Unable to successfully load the beatmap to be usable with this ruleset.</exception>
|
||||||
/// <returns></returns>
|
|
||||||
public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null);
|
public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -62,7 +62,6 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a mapping of <see cref="HitResult"/>s to their timing windows for all allowed <see cref="HitResult"/>s.
|
/// Retrieves a mapping of <see cref="HitResult"/>s to their timing windows for all allowed <see cref="HitResult"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows()
|
public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows()
|
||||||
{
|
{
|
||||||
for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
|
for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
|
||||||
|
@ -10,6 +10,7 @@ using Newtonsoft.Json.Converters;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -55,9 +56,10 @@ namespace osu.Game.Scoring
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public virtual RulesetInfo Ruleset { get; set; }
|
public virtual RulesetInfo Ruleset { get; set; }
|
||||||
|
|
||||||
|
private APIMod[] localAPIMods;
|
||||||
private Mod[] mods;
|
private Mod[] mods;
|
||||||
|
|
||||||
[JsonProperty("mods")]
|
[JsonIgnore]
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public Mod[] Mods
|
public Mod[] Mods
|
||||||
{
|
{
|
||||||
@ -66,43 +68,50 @@ namespace osu.Game.Scoring
|
|||||||
if (mods != null)
|
if (mods != null)
|
||||||
return mods;
|
return mods;
|
||||||
|
|
||||||
if (modsJson == null)
|
if (localAPIMods == null)
|
||||||
return Array.Empty<Mod>();
|
return Array.Empty<Mod>();
|
||||||
|
|
||||||
return getModsFromRuleset(JsonConvert.DeserializeObject<DeserializedMod[]>(modsJson));
|
var rulesetInstance = Ruleset.CreateInstance();
|
||||||
|
return apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
modsJson = null;
|
localAPIMods = null;
|
||||||
mods = value;
|
mods = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mod[] getModsFromRuleset(DeserializedMod[] mods) => Ruleset.CreateInstance().GetAllMods().Where(mod => mods.Any(d => d.Acronym == mod.Acronym)).ToArray();
|
// Used for API serialisation/deserialisation.
|
||||||
|
[JsonProperty("mods")]
|
||||||
|
[NotMapped]
|
||||||
|
private APIMod[] apiMods
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (localAPIMods != null)
|
||||||
|
return localAPIMods;
|
||||||
|
|
||||||
private string modsJson;
|
if (mods == null)
|
||||||
|
return Array.Empty<APIMod>();
|
||||||
|
|
||||||
|
return localAPIMods = mods.Select(m => new APIMod(m)).ToArray();
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
localAPIMods = value;
|
||||||
|
|
||||||
|
// We potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary.
|
||||||
|
mods = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for database serialisation/deserialisation.
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[Column("Mods")]
|
[Column("Mods")]
|
||||||
public string ModsJson
|
public string ModsJson
|
||||||
{
|
{
|
||||||
get
|
get => JsonConvert.SerializeObject(apiMods);
|
||||||
{
|
set => apiMods = JsonConvert.DeserializeObject<APIMod[]>(value);
|
||||||
if (modsJson != null)
|
|
||||||
return modsJson;
|
|
||||||
|
|
||||||
if (mods == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym }));
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
modsJson = value;
|
|
||||||
|
|
||||||
// we potentially can't update this yet due to Ruleset being late-bound, so instead update on read as necessary.
|
|
||||||
mods = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
@ -251,14 +260,6 @@ namespace osu.Game.Scoring
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
protected class DeserializedMod : IMod
|
|
||||||
{
|
|
||||||
public string Acronym { get; set; }
|
|
||||||
|
|
||||||
public bool Equals(IMod other) => Acronym == other?.Acronym;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"{User} playing {Beatmap}";
|
public override string ToString() => $"{User} playing {Beatmap}";
|
||||||
|
|
||||||
public bool Equals(ScoreInfo other)
|
public bool Equals(ScoreInfo other)
|
||||||
|
@ -12,7 +12,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="RadioButton"/> is selected.
|
/// Whether this <see cref="RadioButton"/> is selected.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
|
||||||
public readonly BindableBool Selected;
|
public readonly BindableBool Selected;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
|
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
|
||||||
{
|
{
|
||||||
@ -12,7 +11,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class PointVisualisation : Box
|
public class PointVisualisation : Box
|
||||||
{
|
{
|
||||||
public const float WIDTH = 1;
|
public const float MAX_WIDTH = 4;
|
||||||
|
|
||||||
public PointVisualisation(double startTime)
|
public PointVisualisation(double startTime)
|
||||||
: this()
|
: this()
|
||||||
@ -27,8 +26,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
|
|||||||
RelativePositionAxes = Axes.X;
|
RelativePositionAxes = Axes.X;
|
||||||
RelativeSizeAxes = Axes.Y;
|
RelativeSizeAxes = Axes.Y;
|
||||||
|
|
||||||
Width = WIDTH;
|
Anchor = Anchor.CentreLeft;
|
||||||
EdgeSmoothness = new Vector2(WIDTH, 0);
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
|
Width = MAX_WIDTH;
|
||||||
|
Height = 0.75f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,11 +135,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
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();
|
prepareSelectionMovement();
|
||||||
|
|
||||||
return e.Button == MouseButton.Left;
|
return selectionPerformed || e.Button == MouseButton.Left;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SelectionBlueprint clickedBlueprint;
|
private SelectionBlueprint clickedBlueprint;
|
||||||
@ -154,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
// Deselection should only occur if no selected blueprints are hovered
|
// 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
|
// 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;
|
return true;
|
||||||
|
|
||||||
deselectAll();
|
deselectAll();
|
||||||
@ -177,7 +178,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
protected override void OnMouseUp(MouseUpEvent e)
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
{
|
{
|
||||||
// Special case for when a drag happened instead of a click
|
// Special case for when a drag happened instead of a click
|
||||||
Schedule(() => endClickSelection());
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
endClickSelection(e);
|
||||||
|
clickSelectionBegan = false;
|
||||||
|
isDraggingBlueprint = false;
|
||||||
|
});
|
||||||
|
|
||||||
finishSelectionMovement();
|
finishSelectionMovement();
|
||||||
}
|
}
|
||||||
@ -226,7 +232,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
Beatmap.Update(obj);
|
Beatmap.Update(obj);
|
||||||
|
|
||||||
changeHandler?.EndChange();
|
changeHandler?.EndChange();
|
||||||
isDraggingBlueprint = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DragBox.State == Visibility.Visible)
|
if (DragBox.State == Visibility.Visible)
|
||||||
@ -338,7 +343,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="e">The input event that triggered this selection.</param>
|
/// <param name="e">The input event that triggered this selection.</param>
|
||||||
/// <returns>Whether a selection was performed.</returns>
|
/// <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).
|
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
|
||||||
// Priority is given to already-selected blueprints.
|
// Priority is given to already-selected blueprints.
|
||||||
@ -346,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
{
|
{
|
||||||
if (!blueprint.IsHovered) continue;
|
if (!blueprint.IsHovered) continue;
|
||||||
|
|
||||||
return clickSelectionBegan = SelectionHandler.HandleSelectionRequested(blueprint, e);
|
return clickSelectionBegan = SelectionHandler.MouseDownSelectionRequested(blueprint, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -355,13 +360,28 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Finishes the current blueprint selection.
|
/// Finishes the current blueprint selection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="e">The mouse event which triggered end of selection.</param>
|
||||||
/// <returns>Whether a click selection was active.</returns>
|
/// <returns>Whether a click selection was active.</returns>
|
||||||
private bool endClickSelection()
|
private bool endClickSelection(MouseButtonEvent e)
|
||||||
{
|
{
|
||||||
if (!clickSelectionBegan)
|
if (!clickSelectionBegan && !isDraggingBlueprint)
|
||||||
return false;
|
{
|
||||||
|
// 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,20 +220,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
/// <param name="blueprint">The blueprint.</param>
|
/// <param name="blueprint">The blueprint.</param>
|
||||||
/// <param name="e">The mouse event responsible for selection.</param>
|
/// <param name="e">The mouse event responsible for selection.</param>
|
||||||
/// <returns>Whether a selection was performed.</returns>
|
/// <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)
|
if (e.ShiftPressed && e.Button == MouseButton.Right)
|
||||||
{
|
{
|
||||||
handleQuickDeletion(blueprint);
|
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();
|
blueprint.ToggleSelection();
|
||||||
else
|
return true;
|
||||||
ensureSelected(blueprint);
|
}
|
||||||
|
|
||||||
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)
|
private void handleQuickDeletion(SelectionBlueprint blueprint)
|
||||||
@ -247,13 +266,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
deleteSelected();
|
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)
|
if (blueprint.IsSelected)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
DeselectAll?.Invoke();
|
DeselectAll?.Invoke();
|
||||||
blueprint.Select();
|
blueprint.Select();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteSelected()
|
private void deleteSelected()
|
||||||
|
@ -63,7 +63,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
{
|
{
|
||||||
AddInternal(backgroundBox = new SelectableAreaBackground
|
AddInternal(backgroundBox = new SelectableAreaBackground
|
||||||
{
|
{
|
||||||
Colour = Color4.Black
|
Colour = Color4.Black,
|
||||||
|
Depth = float.MaxValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -16,7 +17,6 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -28,9 +28,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
{
|
{
|
||||||
public class TimelineHitObjectBlueprint : SelectionBlueprint
|
public class TimelineHitObjectBlueprint : SelectionBlueprint
|
||||||
{
|
{
|
||||||
private const float thickness = 5;
|
private const float circle_size = 38;
|
||||||
private const float shadow_radius = 5;
|
|
||||||
private const float circle_size = 34;
|
private Container repeatsContainer;
|
||||||
|
|
||||||
public Action<DragEvent> OnDragHandled;
|
public Action<DragEvent> OnDragHandled;
|
||||||
|
|
||||||
@ -40,10 +40,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
private Bindable<int> indexInCurrentComboBindable;
|
private Bindable<int> indexInCurrentComboBindable;
|
||||||
private Bindable<int> comboIndexBindable;
|
private Bindable<int> comboIndexBindable;
|
||||||
|
|
||||||
private readonly Circle circle;
|
private readonly Drawable circle;
|
||||||
private readonly DragBar dragBar;
|
|
||||||
private readonly List<Container> shadowComponents = new List<Container>();
|
private readonly Container colouredComponents;
|
||||||
private readonly Container mainComponents;
|
|
||||||
private readonly OsuSpriteText comboIndexText;
|
private readonly OsuSpriteText comboIndexText;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -61,89 +60,41 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
RelativePositionAxes = Axes.X;
|
RelativePositionAxes = Axes.X;
|
||||||
|
|
||||||
RelativeSizeAxes = 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,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.Both,
|
||||||
AutoSizeAxes = Axes.Y,
|
Children = new Drawable[]
|
||||||
},
|
{
|
||||||
comboIndexText = new OsuSpriteText
|
comboIndexText = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black),
|
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)
|
if (hitObject is IHasDuration)
|
||||||
{
|
{
|
||||||
DragBar dragBarUnderlay;
|
colouredComponents.Add(new DragArea(hitObject)
|
||||||
Container extensionBar;
|
|
||||||
|
|
||||||
mainComponents.AddRange(new Drawable[]
|
|
||||||
{
|
{
|
||||||
extensionBar = new Container
|
OnDragHandled = e => OnDragHandled?.Invoke(e)
|
||||||
{
|
|
||||||
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) },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
shadowComponents.Add(dragBarUnderlay);
|
|
||||||
shadowComponents.Add(extensionBar);
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
mainComponents.Add(circle);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateShadows();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
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 updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString();
|
||||||
|
|
||||||
private void updateComboColour()
|
private void updateComboColour()
|
||||||
@ -173,15 +134,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
var comboColour = combo.GetComboColour(comboColours);
|
var comboColour = combo.GetComboColour(comboColours);
|
||||||
|
|
||||||
if (HitObject is IHasDuration)
|
if (HitObject is IHasDuration)
|
||||||
mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White);
|
circle.Colour = ColourInfo.GradientHorizontal(comboColour, comboColour.Lighten(0.4f));
|
||||||
else
|
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;
|
float brightness = col.R + col.G + col.B;
|
||||||
|
|
||||||
// decide the combo index colour based on brightness?
|
// 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()
|
protected override void Update()
|
||||||
@ -201,13 +162,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Container repeatsContainer;
|
|
||||||
|
|
||||||
private void updateRepeats(IHasRepeats repeats)
|
private void updateRepeats(IHasRepeats repeats)
|
||||||
{
|
{
|
||||||
repeatsContainer?.Expire();
|
repeatsContainer?.Expire();
|
||||||
|
|
||||||
mainComponents.Add(repeatsContainer = new Container
|
colouredComponents.Add(repeatsContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
});
|
});
|
||||||
@ -216,7 +175,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
{
|
{
|
||||||
repeatsContainer.Add(new Circle
|
repeatsContainer.Add(new Circle
|
||||||
{
|
{
|
||||||
Size = new Vector2(circle_size / 2),
|
Size = new Vector2(circle_size / 3),
|
||||||
|
Alpha = 0.2f,
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
RelativePositionAxes = Axes.X,
|
RelativePositionAxes = Axes.X,
|
||||||
@ -228,61 +188,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
protected override bool ShouldBeConsideredForInput(Drawable child) => true;
|
protected override bool ShouldBeConsideredForInput(Drawable child) => true;
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||||
base.ReceivePositionalInputAt(screenSpacePos) ||
|
circle.ReceivePositionalInputAt(screenSpacePos);
|
||||||
circle.ReceivePositionalInputAt(screenSpacePos) ||
|
|
||||||
dragBar?.ReceivePositionalInputAt(screenSpacePos) == true;
|
|
||||||
|
|
||||||
protected override void OnSelected()
|
public override Quad SelectionQuad => circle.ScreenSpaceDrawQuad;
|
||||||
{
|
|
||||||
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 Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;
|
public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;
|
||||||
|
|
||||||
public class DragBar : Container
|
public class DragArea : Circle
|
||||||
{
|
{
|
||||||
private readonly HitObject hitObject;
|
private readonly HitObject hitObject;
|
||||||
|
|
||||||
@ -293,13 +205,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
public override bool HandlePositionalInput => hitObject != null;
|
public override bool HandlePositionalInput => hitObject != null;
|
||||||
|
|
||||||
public DragBar(HitObject hitObject)
|
public DragArea(HitObject hitObject)
|
||||||
{
|
{
|
||||||
this.hitObject = hitObject;
|
this.hitObject = hitObject;
|
||||||
|
|
||||||
CornerRadius = 2;
|
CornerRadius = circle_size / 2;
|
||||||
Masking = true;
|
Masking = true;
|
||||||
Size = new Vector2(5, 1);
|
Size = new Vector2(circle_size, 1);
|
||||||
Anchor = Anchor.CentreRight;
|
Anchor = Anchor.CentreRight;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
RelativePositionAxes = Axes.X;
|
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)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
updateState();
|
updateState();
|
||||||
@ -345,7 +265,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
private void updateState()
|
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]
|
[Resolved]
|
||||||
@ -406,5 +339,37 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
changeHandler?.EndChange();
|
changeHandler?.EndChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A circle with externalised end caps so it can take up the full width of a relative width area.
|
||||||
|
/// </summary>
|
||||||
|
public class ExtendableCircle : CompositeDrawable
|
||||||
|
{
|
||||||
|
private readonly CircularContainer content;
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => content.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
|
public override Quad ScreenSpaceDrawQuad => content.ScreenSpaceDrawQuad;
|
||||||
|
|
||||||
|
public ExtendableCircle()
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding { Horizontal = -circle_size / 2f };
|
||||||
|
InternalChild = content = new CircularContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = true,
|
||||||
|
EdgeEffect = new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Type = EdgeEffectType.Shadow,
|
||||||
|
Radius = 5,
|
||||||
|
Colour = Color4.Black.Opacity(0.4f)
|
||||||
|
},
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,7 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Caching;
|
using osu.Framework.Caching;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
|
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
|
||||||
@ -33,6 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
private static readonly int highest_divisor = BindableBeatDivisor.VALID_DIVISORS.Last();
|
||||||
|
|
||||||
public TimelineTickDisplay()
|
public TimelineTickDisplay()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -80,8 +80,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
if (timeline != null)
|
if (timeline != null)
|
||||||
{
|
{
|
||||||
var newRange = (
|
var newRange = (
|
||||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
|
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
|
||||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
|
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
|
||||||
|
|
||||||
if (visibleRange != newRange)
|
if (visibleRange != newRange)
|
||||||
{
|
{
|
||||||
@ -100,7 +100,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
private void createTicks()
|
private void createTicks()
|
||||||
{
|
{
|
||||||
int drawableIndex = 0;
|
int drawableIndex = 0;
|
||||||
int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last();
|
|
||||||
|
|
||||||
nextMinTick = null;
|
nextMinTick = null;
|
||||||
nextMaxTick = null;
|
nextMaxTick = null;
|
||||||
@ -131,25 +130,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
|
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
|
||||||
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
|
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
|
||||||
|
|
||||||
bool isMainBeat = indexInBar == 0;
|
|
||||||
|
|
||||||
// even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
|
// even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
|
||||||
float height = isMainBeat ? 0.5f : 0.4f - (float)divisor / highestDivisor * 0.2f;
|
|
||||||
float gradientOpacity = isMainBeat ? 1 : 0;
|
|
||||||
|
|
||||||
var topPoint = getNextUsablePoint();
|
var line = getNextUsableLine();
|
||||||
topPoint.X = xPos;
|
line.X = xPos;
|
||||||
topPoint.Height = height;
|
line.Width = PointVisualisation.MAX_WIDTH * getWidth(indexInBar, divisor);
|
||||||
topPoint.Colour = ColourInfo.GradientVertical(colour, colour.Opacity(gradientOpacity));
|
line.Height = 0.9f * getHeight(indexInBar, divisor);
|
||||||
topPoint.Anchor = Anchor.TopLeft;
|
line.Colour = colour;
|
||||||
topPoint.Origin = Anchor.TopCentre;
|
|
||||||
|
|
||||||
var bottomPoint = getNextUsablePoint();
|
|
||||||
bottomPoint.X = xPos;
|
|
||||||
bottomPoint.Anchor = Anchor.BottomLeft;
|
|
||||||
bottomPoint.Colour = ColourInfo.GradientVertical(colour.Opacity(gradientOpacity), colour);
|
|
||||||
bottomPoint.Origin = Anchor.BottomCentre;
|
|
||||||
bottomPoint.Height = height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beat++;
|
beat++;
|
||||||
@ -168,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
tickCache.Validate();
|
tickCache.Validate();
|
||||||
|
|
||||||
Drawable getNextUsablePoint()
|
Drawable getNextUsableLine()
|
||||||
{
|
{
|
||||||
PointVisualisation point;
|
PointVisualisation point;
|
||||||
if (drawableIndex >= Count)
|
if (drawableIndex >= Count)
|
||||||
@ -183,6 +170,54 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static float getWidth(int indexInBar, int divisor)
|
||||||
|
{
|
||||||
|
if (indexInBar == 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
switch (divisor)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
return 0.6f;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
return 0.5f;
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
case 8:
|
||||||
|
return 0.4f;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0.3f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float getHeight(int indexInBar, int divisor)
|
||||||
|
{
|
||||||
|
if (indexInBar == 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
switch (divisor)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
case 2:
|
||||||
|
return 0.9f;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
return 0.8f;
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
case 8:
|
||||||
|
return 0.7f;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0.6f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -19,6 +19,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
|
|||||||
|
|
||||||
protected OnlinePlayComposite Settings { get; set; }
|
protected OnlinePlayComposite Settings { get; set; }
|
||||||
|
|
||||||
|
protected override bool BlockScrollInput => false;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard
|
||||||
|
{
|
||||||
|
public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds)
|
||||||
|
: base(scoreProcessor, userIds)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddClock(int userId, IClock clock)
|
||||||
|
{
|
||||||
|
if (!UserScores.TryGetValue(userId, out var data))
|
||||||
|
return;
|
||||||
|
|
||||||
|
((SpectatingTrackedUserData)data).Clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveClock(int userId)
|
||||||
|
{
|
||||||
|
if (!UserScores.TryGetValue(userId, out var data))
|
||||||
|
return;
|
||||||
|
|
||||||
|
((SpectatingTrackedUserData)data).Clock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(userId, scoreProcessor);
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
foreach (var (_, data) in UserScores)
|
||||||
|
data.UpdateScore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SpectatingTrackedUserData : TrackedUserData
|
||||||
|
{
|
||||||
|
[CanBeNull]
|
||||||
|
public IClock Clock;
|
||||||
|
|
||||||
|
public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor)
|
||||||
|
: base(userId, scoreProcessor)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void UpdateScore()
|
||||||
|
{
|
||||||
|
if (Frames.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Clock == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int frameIndex = Frames.BinarySearch(new TimedFrame(Clock.CurrentTime));
|
||||||
|
if (frameIndex < 0)
|
||||||
|
frameIndex = ~frameIndex;
|
||||||
|
frameIndex = Math.Clamp(frameIndex - 1, 0, Frames.Count - 1);
|
||||||
|
|
||||||
|
SetFrame(Frames[frameIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
167
osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs
Normal file
167
osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerGrid.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A grid of players playing the multiplayer match.
|
||||||
|
/// </summary>
|
||||||
|
public partial class PlayerGrid : CompositeDrawable
|
||||||
|
{
|
||||||
|
private const float player_spacing = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The currently-maximised facade.
|
||||||
|
/// </summary>
|
||||||
|
public Drawable MaximisedFacade => maximisedFacade;
|
||||||
|
|
||||||
|
private readonly Facade maximisedFacade;
|
||||||
|
private readonly Container paddingContainer;
|
||||||
|
private readonly FillFlowContainer<Facade> facadeContainer;
|
||||||
|
private readonly Container<Cell> cellContainer;
|
||||||
|
|
||||||
|
public PlayerGrid()
|
||||||
|
{
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
paddingContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding(player_spacing),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = facadeContainer = new FillFlowContainer<Facade>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(player_spacing),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maximisedFacade = new Facade { RelativeSizeAxes = Axes.Both }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cellContainer = new Container<Cell> { RelativeSizeAxes = Axes.Both }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a new cell with content to this grid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="content">The content the cell should contain.</param>
|
||||||
|
/// <exception cref="InvalidOperationException">If more than 16 cells are added.</exception>
|
||||||
|
public void Add(Drawable content)
|
||||||
|
{
|
||||||
|
if (cellContainer.Count == 16)
|
||||||
|
throw new InvalidOperationException("Only 16 cells are supported.");
|
||||||
|
|
||||||
|
int index = cellContainer.Count;
|
||||||
|
|
||||||
|
var facade = new Facade();
|
||||||
|
facadeContainer.Add(facade);
|
||||||
|
|
||||||
|
var cell = new Cell(index, content) { ToggleMaximisationState = toggleMaximisationState };
|
||||||
|
cell.SetFacade(facade);
|
||||||
|
|
||||||
|
cellContainer.Add(cell);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The content added to this grid.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Drawable> Content => cellContainer.OrderBy(c => c.FacadeIndex).Select(c => c.Content);
|
||||||
|
|
||||||
|
// A depth value that gets decremented every time a new instance is maximised in order to reduce underlaps.
|
||||||
|
private float maximisedInstanceDepth;
|
||||||
|
|
||||||
|
private void toggleMaximisationState(Cell target)
|
||||||
|
{
|
||||||
|
// Iterate through all cells to ensure only one is maximised at any time.
|
||||||
|
foreach (var i in cellContainer.ToList())
|
||||||
|
{
|
||||||
|
if (i == target)
|
||||||
|
i.IsMaximised = !i.IsMaximised;
|
||||||
|
else
|
||||||
|
i.IsMaximised = false;
|
||||||
|
|
||||||
|
if (i.IsMaximised)
|
||||||
|
{
|
||||||
|
// Transfer cell to the maximised facade.
|
||||||
|
i.SetFacade(maximisedFacade);
|
||||||
|
cellContainer.ChangeChildDepth(i, maximisedInstanceDepth -= 0.001f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Transfer cell back to its original facade.
|
||||||
|
i.SetFacade(facadeContainer[i.FacadeIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
// Different layouts are used for varying cell counts in order to maximise dimensions.
|
||||||
|
Vector2 cellsPerDimension;
|
||||||
|
|
||||||
|
switch (facadeContainer.Count)
|
||||||
|
{
|
||||||
|
case 1:
|
||||||
|
cellsPerDimension = Vector2.One;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
cellsPerDimension = new Vector2(2, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
case 4:
|
||||||
|
cellsPerDimension = new Vector2(2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
case 6:
|
||||||
|
cellsPerDimension = new Vector2(3, 2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 7:
|
||||||
|
case 8:
|
||||||
|
case 9:
|
||||||
|
// 3 rows / 3 cols.
|
||||||
|
cellsPerDimension = new Vector2(3);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 10:
|
||||||
|
case 11:
|
||||||
|
case 12:
|
||||||
|
// 3 rows / 4 cols.
|
||||||
|
cellsPerDimension = new Vector2(4, 3);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// 4 rows / 4 cols.
|
||||||
|
cellsPerDimension = new Vector2(4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total inter-cell spacing.
|
||||||
|
Vector2 totalCellSpacing = player_spacing * (cellsPerDimension - Vector2.One);
|
||||||
|
|
||||||
|
Vector2 fullSize = paddingContainer.ChildSize - totalCellSpacing;
|
||||||
|
Vector2 cellSize = Vector2.Divide(fullSize, new Vector2(cellsPerDimension.X, cellsPerDimension.Y));
|
||||||
|
|
||||||
|
foreach (var cell in facadeContainer)
|
||||||
|
cell.Size = cellSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
public partial class PlayerGrid
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A cell of the grid. Contains the content and tracks to the linked facade.
|
||||||
|
/// </summary>
|
||||||
|
private class Cell : CompositeDrawable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the original facade of this cell.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int FacadeIndex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The contained content.
|
||||||
|
/// </summary>
|
||||||
|
public readonly Drawable Content;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An action that toggles the maximisation state of this cell.
|
||||||
|
/// </summary>
|
||||||
|
public Action<Cell> ToggleMaximisationState;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this cell is currently maximised.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsMaximised;
|
||||||
|
|
||||||
|
private Facade facade;
|
||||||
|
private bool isTracking = true;
|
||||||
|
|
||||||
|
public Cell(int facadeIndex, Drawable content)
|
||||||
|
{
|
||||||
|
FacadeIndex = facadeIndex;
|
||||||
|
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
InternalChild = Content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (isTracking)
|
||||||
|
{
|
||||||
|
Position = getFinalPosition();
|
||||||
|
Size = getFinalSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes this cell track a new facade.
|
||||||
|
/// </summary>
|
||||||
|
public void SetFacade([NotNull] Facade newFacade)
|
||||||
|
{
|
||||||
|
Facade lastFacade = facade;
|
||||||
|
facade = newFacade;
|
||||||
|
|
||||||
|
if (lastFacade == null || lastFacade == newFacade)
|
||||||
|
return;
|
||||||
|
|
||||||
|
isTracking = false;
|
||||||
|
|
||||||
|
this.MoveTo(getFinalPosition(), 400, Easing.OutQuint).ResizeTo(getFinalSize(), 400, Easing.OutQuint)
|
||||||
|
.Then()
|
||||||
|
.OnComplete(_ =>
|
||||||
|
{
|
||||||
|
if (facade == newFacade)
|
||||||
|
isTracking = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 getFinalPosition()
|
||||||
|
{
|
||||||
|
var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero));
|
||||||
|
return topLeft + facade.DrawSize / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 getFinalSize() => facade.DrawSize;
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
ToggleMaximisationState(this);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
public partial class PlayerGrid
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A facade of the grid which is used as a dummy object to store the required position/size of cells.
|
||||||
|
/// </summary>
|
||||||
|
private class Facade : Drawable
|
||||||
|
{
|
||||||
|
public Facade()
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre;
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,8 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
protected override bool BlockNonPositionalInput => true;
|
protected override bool BlockNonPositionalInput => true;
|
||||||
|
|
||||||
|
protected override bool BlockScrollInput => false;
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||||
|
|
||||||
public Action OnRetry;
|
public Action OnRetry;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
@ -19,9 +19,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
[LongRunningLoad]
|
[LongRunningLoad]
|
||||||
public class MultiplayerGameplayLeaderboard : GameplayLeaderboard
|
public class MultiplayerGameplayLeaderboard : GameplayLeaderboard
|
||||||
{
|
{
|
||||||
private readonly ScoreProcessor scoreProcessor;
|
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
|
||||||
|
|
||||||
private readonly Dictionary<int, TrackedUserData> userScores = new Dictionary<int, TrackedUserData>();
|
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorStreamingClient streamingClient { get; set; }
|
private SpectatorStreamingClient streamingClient { get; set; }
|
||||||
@ -32,9 +30,9 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private UserLookupCache userLookupCache { get; set; }
|
private UserLookupCache userLookupCache { get; set; }
|
||||||
|
|
||||||
private Bindable<ScoringMode> scoringMode;
|
private readonly ScoreProcessor scoreProcessor;
|
||||||
|
|
||||||
private readonly BindableList<int> playingUsers;
|
private readonly BindableList<int> playingUsers;
|
||||||
|
private Bindable<ScoringMode> scoringMode;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Construct a new leaderboard.
|
/// Construct a new leaderboard.
|
||||||
@ -53,6 +51,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config, IAPIProvider api)
|
private void load(OsuConfigManager config, IAPIProvider api)
|
||||||
{
|
{
|
||||||
|
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
|
||||||
|
|
||||||
foreach (var userId in playingUsers)
|
foreach (var userId in playingUsers)
|
||||||
{
|
{
|
||||||
streamingClient.WatchUser(userId);
|
streamingClient.WatchUser(userId);
|
||||||
@ -60,19 +60,17 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
// probably won't be required in the final implementation.
|
// probably won't be required in the final implementation.
|
||||||
var resolvedUser = userLookupCache.GetUserAsync(userId).Result;
|
var resolvedUser = userLookupCache.GetUserAsync(userId).Result;
|
||||||
|
|
||||||
var trackedUser = new TrackedUserData();
|
var trackedUser = CreateUserData(userId, scoreProcessor);
|
||||||
|
trackedUser.ScoringMode.BindTo(scoringMode);
|
||||||
|
|
||||||
userScores[userId] = trackedUser;
|
|
||||||
var leaderboardScore = AddPlayer(resolvedUser, resolvedUser?.Id == api.LocalUser.Value.Id);
|
var leaderboardScore = AddPlayer(resolvedUser, resolvedUser?.Id == api.LocalUser.Value.Id);
|
||||||
|
leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy);
|
||||||
|
leaderboardScore.TotalScore.BindTo(trackedUser.Score);
|
||||||
|
leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo);
|
||||||
|
leaderboardScore.HasQuit.BindTo(trackedUser.UserQuit);
|
||||||
|
|
||||||
((IBindable<double>)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy);
|
UserScores[userId] = trackedUser;
|
||||||
((IBindable<double>)leaderboardScore.TotalScore).BindTo(trackedUser.Score);
|
|
||||||
((IBindable<int>)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo);
|
|
||||||
((IBindable<bool>)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
|
|
||||||
scoringMode.BindValueChanged(updateAllScores, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -102,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
streamingClient.StopWatchingUser(userId);
|
streamingClient.StopWatchingUser(userId);
|
||||||
|
|
||||||
if (userScores.TryGetValue(userId, out var trackedData))
|
if (UserScores.TryGetValue(userId, out var trackedData))
|
||||||
trackedData.MarkUserQuit();
|
trackedData.MarkUserQuit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,20 +108,16 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAllScores(ValueChangedEvent<ScoringMode> mode)
|
private void handleIncomingFrames(int userId, FrameDataBundle bundle) => Schedule(() =>
|
||||||
{
|
{
|
||||||
foreach (var trackedData in userScores.Values)
|
if (!UserScores.TryGetValue(userId, out var trackedData))
|
||||||
trackedData.UpdateScore(scoreProcessor, mode.NewValue);
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
private void handleIncomingFrames(int userId, FrameDataBundle bundle)
|
trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
|
||||||
{
|
trackedData.UpdateScore();
|
||||||
if (userScores.TryGetValue(userId, out var trackedData))
|
});
|
||||||
{
|
|
||||||
trackedData.LastHeader = bundle.Header;
|
protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor);
|
||||||
trackedData.UpdateScore(scoreProcessor, scoringMode.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
@ -140,38 +134,65 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TrackedUserData
|
protected class TrackedUserData
|
||||||
{
|
{
|
||||||
public IBindableNumber<double> Score => score;
|
public readonly int UserId;
|
||||||
|
public readonly ScoreProcessor ScoreProcessor;
|
||||||
|
|
||||||
private readonly BindableDouble score = new BindableDouble();
|
public readonly BindableDouble Score = new BindableDouble();
|
||||||
|
public readonly BindableDouble Accuracy = new BindableDouble(1);
|
||||||
|
public readonly BindableInt CurrentCombo = new BindableInt();
|
||||||
|
public readonly BindableBool UserQuit = new BindableBool();
|
||||||
|
|
||||||
public IBindableNumber<double> Accuracy => accuracy;
|
public readonly IBindable<ScoringMode> ScoringMode = new Bindable<ScoringMode>();
|
||||||
|
|
||||||
private readonly BindableDouble accuracy = new BindableDouble(1);
|
public readonly List<TimedFrame> Frames = new List<TimedFrame>();
|
||||||
|
|
||||||
public IBindableNumber<int> CurrentCombo => currentCombo;
|
public TrackedUserData(int userId, ScoreProcessor scoreProcessor)
|
||||||
|
|
||||||
private readonly BindableInt currentCombo = new BindableInt();
|
|
||||||
|
|
||||||
public IBindable<bool> UserQuit => userQuit;
|
|
||||||
|
|
||||||
private readonly BindableBool userQuit = new BindableBool();
|
|
||||||
|
|
||||||
[CanBeNull]
|
|
||||||
public FrameHeader LastHeader;
|
|
||||||
|
|
||||||
public void MarkUserQuit() => userQuit.Value = true;
|
|
||||||
|
|
||||||
public void UpdateScore(ScoreProcessor processor, ScoringMode mode)
|
|
||||||
{
|
{
|
||||||
if (LastHeader == null)
|
UserId = userId;
|
||||||
|
ScoreProcessor = scoreProcessor;
|
||||||
|
|
||||||
|
ScoringMode.BindValueChanged(_ => UpdateScore());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MarkUserQuit() => UserQuit.Value = true;
|
||||||
|
|
||||||
|
public virtual void UpdateScore()
|
||||||
|
{
|
||||||
|
if (Frames.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
score.Value = processor.GetImmediateScore(mode, LastHeader.MaxCombo, LastHeader.Statistics);
|
SetFrame(Frames.Last());
|
||||||
accuracy.Value = LastHeader.Accuracy;
|
|
||||||
currentCombo.Value = LastHeader.Combo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void SetFrame(TimedFrame frame)
|
||||||
|
{
|
||||||
|
var header = frame.Header;
|
||||||
|
|
||||||
|
Score.Value = ScoreProcessor.GetImmediateScore(ScoringMode.Value, header.MaxCombo, header.Statistics);
|
||||||
|
Accuracy.Value = header.Accuracy;
|
||||||
|
CurrentCombo.Value = header.Combo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class TimedFrame : IComparable<TimedFrame>
|
||||||
|
{
|
||||||
|
public readonly double Time;
|
||||||
|
public readonly FrameHeader Header;
|
||||||
|
|
||||||
|
public TimedFrame(double time)
|
||||||
|
{
|
||||||
|
Time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimedFrame(double time, FrameHeader header)
|
||||||
|
{
|
||||||
|
Time = time;
|
||||||
|
Header = header;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,8 @@ namespace osu.Game.Screens.Play
|
|||||||
public override bool HandleNonPositionalInput => AllowSeeking.Value;
|
public override bool HandleNonPositionalInput => AllowSeeking.Value;
|
||||||
public override bool HandlePositionalInput => AllowSeeking.Value;
|
public override bool HandlePositionalInput => AllowSeeking.Value;
|
||||||
|
|
||||||
|
protected override bool BlockScrollInput => false;
|
||||||
|
|
||||||
private double firstHitTime => objects.First().StartTime;
|
private double firstHitTime => objects.First().StartTime;
|
||||||
|
|
||||||
private IEnumerable<HitObject> objects;
|
private IEnumerable<HitObject> objects;
|
||||||
|
@ -109,7 +109,12 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
request.Success += s =>
|
request.Success += s =>
|
||||||
{
|
{
|
||||||
score.ScoreInfo.OnlineScoreID = s.ID;
|
// For the time being, online ID responses are not really useful for anything.
|
||||||
|
// In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores.
|
||||||
|
//
|
||||||
|
// Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
|
||||||
|
// conflicts across various systems (ie. solo and multiplayer).
|
||||||
|
// score.ScoreInfo.OnlineScoreID = s.ID;
|
||||||
tcs.SetResult(true);
|
tcs.SetResult(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -226,7 +226,6 @@ namespace osu.Game.Screens.Ranking
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumerates all <see cref="ScorePanel"/>s contained in this <see cref="ScorePanelList"/>.
|
/// Enumerates all <see cref="ScorePanel"/>s contained in this <see cref="ScorePanelList"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<ScorePanel> GetScorePanels() => flow.Select(t => t.Panel);
|
public IEnumerable<ScorePanel> GetScorePanels() => flow.Select(t => t.Panel);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -189,7 +189,6 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the <see cref="Ruleset"/> applicable to this <see cref="BeatmapConversionTest{TConvertMapping,TConvertValue}"/>.
|
/// Creates the <see cref="Ruleset"/> applicable to this <see cref="BeatmapConversionTest{TConvertMapping,TConvertValue}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
|
||||||
protected abstract Ruleset CreateRuleset();
|
protected abstract Ruleset CreateRuleset();
|
||||||
|
|
||||||
private class ConvertResult
|
private class ConvertResult
|
||||||
|
@ -17,7 +17,6 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the <see cref="Ruleset"/> whose legacy mod conversion is to be tested.
|
/// Creates the <see cref="Ruleset"/> whose legacy mod conversion is to be tested.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
|
||||||
protected abstract Ruleset CreateRuleset();
|
protected abstract Ruleset CreateRuleset();
|
||||||
|
|
||||||
protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods)
|
protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods)
|
||||||
|
@ -3,8 +3,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -129,5 +132,38 @@ namespace osu.Game.Utils
|
|||||||
else
|
else
|
||||||
yield return mod;
|
yield return mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the underlying value of the given mod setting object.
|
||||||
|
/// Used in <see cref="APIMod"/> for serialization and equality comparison purposes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="setting">The mod setting.</param>
|
||||||
|
public static object GetSettingUnderlyingValue(object setting)
|
||||||
|
{
|
||||||
|
switch (setting)
|
||||||
|
{
|
||||||
|
case Bindable<double> d:
|
||||||
|
return d.Value;
|
||||||
|
|
||||||
|
case Bindable<int> i:
|
||||||
|
return i.Value;
|
||||||
|
|
||||||
|
case Bindable<float> f:
|
||||||
|
return f.Value;
|
||||||
|
|
||||||
|
case Bindable<bool> b:
|
||||||
|
return b.Value;
|
||||||
|
|
||||||
|
case IBindable u:
|
||||||
|
// A mod with unknown (e.g. enum) generic type.
|
||||||
|
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
|
||||||
|
Debug.Assert(valueMethod != null);
|
||||||
|
return valueMethod.GetValue(u);
|
||||||
|
|
||||||
|
default:
|
||||||
|
// fall back for non-bindable cases.
|
||||||
|
return setting;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,6 @@ namespace osu.Game.Utils
|
|||||||
/// Shortcase for: <c>optional.HasValue ? optional.Value : fallback</c>.
|
/// Shortcase for: <c>optional.HasValue ? optional.Value : fallback</c>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="fallback">The fallback value to return if <see cref="HasValue"/> is <c>false</c>.</param>
|
/// <param name="fallback">The fallback value to return if <see cref="HasValue"/> is <c>false</c>.</param>
|
||||||
/// <returns></returns>
|
|
||||||
public T GetOr(T fallback) => HasValue ? Value : fallback;
|
public T GetOr(T fallback) => HasValue ? Value : fallback;
|
||||||
|
|
||||||
public static implicit operator Optional<T>(T value) => new Optional<T>(value);
|
public static implicit operator Optional<T>(T value) => new Optional<T>(value);
|
||||||
|
@ -29,8 +29,8 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
|
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.410.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.413.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.2.0" />
|
<PackageReference Include="Sentry" Version="3.2.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||||
|
@ -70,8 +70,8 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.410.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.413.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@ -93,7 +93,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.410.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.413.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
<PackageReference Include="SharpCompress" Version="0.28.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||||
|
Loading…
Reference in New Issue
Block a user