mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 10:12:54 +08:00
Merge branch 'master' into refactor-framed-replay-input-hander
This commit is contained in:
commit
38d10755cb
@ -51,7 +51,7 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</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" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -69,7 +69,6 @@ namespace osu.Desktop
|
||||
/// Allow a maximum of one unhandled exception, per second of execution.
|
||||
/// </summary>
|
||||
/// <param name="arg"></param>
|
||||
/// <returns></returns>
|
||||
private static bool handleException(Exception arg)
|
||||
{
|
||||
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.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
|
||||
/// <summary>
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
|
||||
private void runSpmTest(Mod mod)
|
||||
{
|
||||
SpinnerSpmCounter spmCounter = null;
|
||||
SpinnerSpmCalculator spmCalculator = null;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
|
||||
});
|
||||
|
||||
AddUntilStep("fetch SPM counter", () =>
|
||||
AddUntilStep("fetch SPM calculator", () =>
|
||||
{
|
||||
spmCounter = this.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
||||
return spmCounter != null;
|
||||
spmCalculator = this.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||
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,
|
||||
PassCondition = () =>
|
||||
{
|
||||
var counter = Player.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
||||
return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
|
||||
var counter = Player.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||
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;
|
||||
|
||||
addSeekStep(1000);
|
||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
|
||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
|
||||
|
||||
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);
|
||||
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)]
|
||||
@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("retrieve spinner state", () =>
|
||||
{
|
||||
expectedProgress = drawableSpinner.Progress;
|
||||
expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
|
||||
expectedSpm = drawableSpinner.SpinsPerMinute.Value;
|
||||
});
|
||||
|
||||
addSeekStep(0);
|
||||
@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
addSeekStep(1000);
|
||||
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
|
||||
|
@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
||||
|
||||
public SpinnerRotationTracker RotationTracker { get; private set; }
|
||||
public SpinnerSpmCounter SpmCounter { get; private set; }
|
||||
|
||||
private SpinnerSpmCalculator spmCalculator;
|
||||
|
||||
private Container<DrawableSpinnerTick> ticks;
|
||||
private PausableSkinnableSound spinningSample;
|
||||
@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@ -63,8 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Origin = Anchor.Centre;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
spmCalculator = new SpinnerSpmCalculator
|
||||
{
|
||||
Result = { BindTarget = SpinsPerMinute },
|
||||
},
|
||||
ticks = new Container<DrawableSpinnerTick>(),
|
||||
new AspectContainer
|
||||
{
|
||||
@ -77,20 +87,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
RotationTracker = new SpinnerRotationTracker(this)
|
||||
}
|
||||
},
|
||||
SpmCounter = new SpinnerSpmCounter
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = 120,
|
||||
Alpha = 0
|
||||
},
|
||||
spinningSample = new PausableSkinnableSound
|
||||
{
|
||||
Volume = { Value = 0 },
|
||||
Looping = true,
|
||||
Frequency = { Value = spinning_sample_initial_frequency }
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
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)
|
||||
{
|
||||
base.UpdateHitStateTransforms(state);
|
||||
@ -282,22 +274,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
|
||||
{
|
||||
Result.TimeStarted ??= Time.Current;
|
||||
fadeInCounter();
|
||||
}
|
||||
if (Result.TimeStarted == null && RotationTracker.Tracking)
|
||||
Result.TimeStarted = Time.Current;
|
||||
|
||||
// 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.
|
||||
if (Time.Current <= HitObject.EndTime)
|
||||
SpmCounter.SetRotation(Result.RateAdjustedRotation);
|
||||
spmCalculator.SetRotation(Result.RateAdjustedRotation);
|
||||
|
||||
updateBonusScore();
|
||||
}
|
||||
|
||||
private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
|
||||
|
||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||
|
||||
private int wholeSpins;
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
private OsuSpriteText bonusCounter;
|
||||
|
||||
private Container spmContainer;
|
||||
private OsuSpriteText spmCounter;
|
||||
|
||||
public DefaultSpinner()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Numeric.With(size: 24),
|
||||
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> spinsPerMinute;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
bonusCounter.FadeOutFromOne(1500);
|
||||
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.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
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]
|
||||
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()
|
||||
{
|
||||
base.LoadComplete();
|
||||
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)
|
||||
{
|
||||
// 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)
|
||||
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 });
|
||||
@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
private void resetState(DrawableHitObject hitObject)
|
||||
{
|
||||
SpinsPerMinute = 0;
|
||||
result.Value = 0;
|
||||
records.Clear();
|
||||
}
|
||||
|
||||
@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
if (drawableSpinner != null)
|
||||
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;
|
||||
|
||||
private const float spm_hide_offset = 50f;
|
||||
|
||||
protected DrawableSpinner DrawableSpinner { get; private set; }
|
||||
|
||||
private Sprite spin;
|
||||
@ -35,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
private LegacySpriteText bonusCounter;
|
||||
|
||||
private Sprite spmBackground;
|
||||
private LegacySpriteText spmCounter;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
|
||||
{
|
||||
@ -79,11 +84,27 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Y = SPINNER_TOP_OFFSET + 299,
|
||||
}.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> spinsPerMinute;
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
spinsPerMinute = DrawableSpinner.SpinsPerMinute.GetBoundCopy();
|
||||
spinsPerMinute.BindValueChanged(spm =>
|
||||
{
|
||||
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||
}, true);
|
||||
|
||||
completed.BindValueChanged(onCompletedChanged, true);
|
||||
|
||||
DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms;
|
||||
@ -142,10 +169,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
switch (drawableHitObject)
|
||||
{
|
||||
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))
|
||||
spin.FadeOutFromOne(fadeOutLength);
|
||||
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
|
||||
|
||||
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
|
||||
spin.FadeOutFromOne(spinFadeOutLength);
|
||||
break;
|
||||
|
||||
case DrawableSpinnerTick d:
|
||||
|
@ -11,7 +11,10 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Tests.Online
|
||||
{
|
||||
@ -84,6 +87,36 @@ namespace osu.Game.Tests.Online
|
||||
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
|
||||
{
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online;
|
||||
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 };
|
||||
|
||||
private OsuConfigManager config;
|
||||
|
||||
public TestSceneMultiplayerGameplayLeaderboard()
|
||||
{
|
||||
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]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
@ -97,6 +106,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
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 new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
|
||||
@ -163,7 +180,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
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);
|
||||
});
|
||||
|
||||
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}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -141,7 +141,6 @@ namespace osu.Game.Tournament
|
||||
/// <summary>
|
||||
/// Add missing player info based on user IDs.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private bool addPlayers()
|
||||
{
|
||||
bool addedInfo = false;
|
||||
|
@ -44,7 +44,6 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Returns statistics for the <see cref="HitObjects"/> contained in this beatmap.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<BeatmapStatistic> GetStatistics();
|
||||
|
||||
/// <summary>
|
||||
|
@ -22,7 +22,6 @@ namespace osu.Game.Configuration
|
||||
/// </summary>
|
||||
/// <param name="rulesetId">The ruleset's internal ID.</param>
|
||||
/// <param name="variant">An optional variant.</param>
|
||||
/// <returns></returns>
|
||||
public List<DatabasedSetting> Query(int? rulesetId = null, int? variant = null) =>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
/// <returns></returns>
|
||||
public int CompareTo(TriangleParticle other) => other.Scale.CompareTo(Scale);
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,6 @@ namespace osu.Game.IO.Serialization
|
||||
/// <summary>
|
||||
/// Creates the default <see cref="JsonSerializerSettings"/> that should be used for all <see cref="IJsonSerializable"/>s.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
|
@ -85,7 +85,6 @@ namespace osu.Game.Input
|
||||
/// </summary>
|
||||
/// <param name="rulesetId">The ruleset's internal ID.</param>
|
||||
/// <param name="variant">An optional variant.</param>
|
||||
/// <returns></returns>
|
||||
public List<DatabasedKeyBinding> Query(int? rulesetId = null, int? variant = null) =>
|
||||
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
||||
|
||||
|
@ -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).
|
||||
/// </summary>
|
||||
/// <param name="position">The lookup X position. Generally should be <see cref="SamplePlaybackPosition"/>.</param>
|
||||
/// <returns></returns>
|
||||
protected double CalculateSamplePlaybackBalance(double position)
|
||||
{
|
||||
const float balance_adjust_amount = 0.4f;
|
||||
|
@ -147,7 +147,6 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// to 1 (end of the path).
|
||||
/// </summary>
|
||||
/// <param name="progress">Ranges from 0 (beginning of the path) to 1 (end of the path).</param>
|
||||
/// <returns></returns>
|
||||
public Vector2 PositionAt(double progress)
|
||||
{
|
||||
ensureValid();
|
||||
@ -161,7 +160,6 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// The first point has a PathType which all other points inherit.
|
||||
/// </summary>
|
||||
/// <param name="controlPoint">One of the control points in the segment.</param>
|
||||
/// <returns></returns>
|
||||
public List<PathControlPoint> PointsInSegment(PathControlPoint controlPoint)
|
||||
{
|
||||
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="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>
|
||||
/// <returns></returns>
|
||||
public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null);
|
||||
|
||||
/// <summary>
|
||||
|
@ -62,7 +62,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// <summary>
|
||||
/// Retrieves a mapping of <see cref="HitResult"/>s to their timing windows for all allowed <see cref="HitResult"/>s.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<(HitResult result, double length)> GetAllAvailableWindows()
|
||||
{
|
||||
for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
|
||||
|
@ -10,6 +10,7 @@ using Newtonsoft.Json.Converters;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -55,9 +56,10 @@ namespace osu.Game.Scoring
|
||||
[JsonIgnore]
|
||||
public virtual RulesetInfo Ruleset { get; set; }
|
||||
|
||||
private APIMod[] localAPIMods;
|
||||
private Mod[] mods;
|
||||
|
||||
[JsonProperty("mods")]
|
||||
[JsonIgnore]
|
||||
[NotMapped]
|
||||
public Mod[] Mods
|
||||
{
|
||||
@ -66,43 +68,50 @@ namespace osu.Game.Scoring
|
||||
if (mods != null)
|
||||
return mods;
|
||||
|
||||
if (modsJson == null)
|
||||
if (localAPIMods == null)
|
||||
return Array.Empty<Mod>();
|
||||
|
||||
return getModsFromRuleset(JsonConvert.DeserializeObject<DeserializedMod[]>(modsJson));
|
||||
var rulesetInstance = Ruleset.CreateInstance();
|
||||
return apiMods.Select(m => m.ToMod(rulesetInstance)).ToArray();
|
||||
}
|
||||
set
|
||||
{
|
||||
modsJson = null;
|
||||
localAPIMods = null;
|
||||
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]
|
||||
[Column("Mods")]
|
||||
public string ModsJson
|
||||
{
|
||||
get
|
||||
{
|
||||
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;
|
||||
}
|
||||
get => JsonConvert.SerializeObject(apiMods);
|
||||
set => apiMods = JsonConvert.DeserializeObject<APIMod[]>(value);
|
||||
}
|
||||
|
||||
[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 bool Equals(ScoreInfo other)
|
||||
|
@ -12,7 +12,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
/// <summary>
|
||||
/// Whether this <see cref="RadioButton"/> is selected.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public readonly BindableBool Selected;
|
||||
|
||||
/// <summary>
|
||||
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
// 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.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
@ -19,9 +19,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[LongRunningLoad]
|
||||
public class MultiplayerGameplayLeaderboard : GameplayLeaderboard
|
||||
{
|
||||
private readonly ScoreProcessor scoreProcessor;
|
||||
|
||||
private readonly Dictionary<int, TrackedUserData> userScores = new Dictionary<int, TrackedUserData>();
|
||||
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
|
||||
|
||||
[Resolved]
|
||||
private SpectatorStreamingClient streamingClient { get; set; }
|
||||
@ -32,9 +30,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; }
|
||||
|
||||
private Bindable<ScoringMode> scoringMode;
|
||||
|
||||
private readonly ScoreProcessor scoreProcessor;
|
||||
private readonly BindableList<int> playingUsers;
|
||||
private Bindable<ScoringMode> scoringMode;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new leaderboard.
|
||||
@ -53,6 +51,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config, IAPIProvider api)
|
||||
{
|
||||
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
|
||||
|
||||
foreach (var userId in playingUsers)
|
||||
{
|
||||
streamingClient.WatchUser(userId);
|
||||
@ -60,19 +60,17 @@ namespace osu.Game.Screens.Play.HUD
|
||||
// probably won't be required in the final implementation.
|
||||
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);
|
||||
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);
|
||||
((IBindable<double>)leaderboardScore.TotalScore).BindTo(trackedUser.Score);
|
||||
((IBindable<int>)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo);
|
||||
((IBindable<bool>)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit);
|
||||
UserScores[userId] = trackedUser;
|
||||
}
|
||||
|
||||
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
|
||||
scoringMode.BindValueChanged(updateAllScores, true);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -102,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
streamingClient.StopWatchingUser(userId);
|
||||
|
||||
if (userScores.TryGetValue(userId, out var trackedData))
|
||||
if (UserScores.TryGetValue(userId, out var trackedData))
|
||||
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)
|
||||
trackedData.UpdateScore(scoreProcessor, mode.NewValue);
|
||||
}
|
||||
if (!UserScores.TryGetValue(userId, out var trackedData))
|
||||
return;
|
||||
|
||||
private void handleIncomingFrames(int userId, FrameDataBundle bundle)
|
||||
{
|
||||
if (userScores.TryGetValue(userId, out var trackedData))
|
||||
{
|
||||
trackedData.LastHeader = bundle.Header;
|
||||
trackedData.UpdateScore(scoreProcessor, scoringMode.Value);
|
||||
}
|
||||
}
|
||||
trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
|
||||
trackedData.UpdateScore();
|
||||
});
|
||||
|
||||
protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor);
|
||||
|
||||
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;
|
||||
|
||||
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)
|
||||
public TrackedUserData(int userId, ScoreProcessor scoreProcessor)
|
||||
{
|
||||
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;
|
||||
|
||||
score.Value = processor.GetImmediateScore(mode, LastHeader.MaxCombo, LastHeader.Statistics);
|
||||
accuracy.Value = LastHeader.Accuracy;
|
||||
currentCombo.Value = LastHeader.Combo;
|
||||
SetFrame(Frames.Last());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -226,7 +226,6 @@ namespace osu.Game.Screens.Ranking
|
||||
/// <summary>
|
||||
/// Enumerates all <see cref="ScorePanel"/>s contained in this <see cref="ScorePanelList"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<ScorePanel> GetScorePanels() => flow.Select(t => t.Panel);
|
||||
|
||||
/// <summary>
|
||||
|
@ -189,7 +189,6 @@ namespace osu.Game.Tests.Beatmaps
|
||||
/// <summary>
|
||||
/// Creates the <see cref="Ruleset"/> applicable to this <see cref="BeatmapConversionTest{TConvertMapping,TConvertValue}"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract Ruleset CreateRuleset();
|
||||
|
||||
private class ConvertResult
|
||||
|
@ -17,7 +17,6 @@ namespace osu.Game.Tests.Beatmaps
|
||||
/// <summary>
|
||||
/// Creates the <see cref="Ruleset"/> whose legacy mod conversion is to be tested.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract Ruleset CreateRuleset();
|
||||
|
||||
protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods)
|
||||
|
@ -37,7 +37,6 @@ namespace osu.Game.Utils
|
||||
/// Shortcase for: <c>optional.HasValue ? optional.Value : fallback</c>.
|
||||
/// </remarks>
|
||||
/// <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 static implicit operator Optional<T>(T value) => new Optional<T>(value);
|
||||
|
@ -30,7 +30,7 @@
|
||||
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.410.0" />
|
||||
<PackageReference Include="ppy.osu.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="SharpCompress" Version="0.28.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
|
@ -71,7 +71,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.410.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.410.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.412.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||
<PropertyGroup>
|
||||
|
Loading…
Reference in New Issue
Block a user