1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 21:27:24 +08:00

Merge branch 'master' into hold-note-lighting

This commit is contained in:
Dean Herbert 2020-08-28 18:27:29 +09:00 committed by GitHub
commit da34544fdc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 516 additions and 70 deletions

View File

@ -326,6 +326,16 @@ namespace osu.Game.Rulesets.Mania
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(score.HitEvents)
}))
}
}
};
}

View File

@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
if (GetColumnSkinConfig<bool>(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false)
Column.UnderlayElements.Add(CreateProxy());
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)

View File

@ -79,7 +79,6 @@ namespace osu.Game.Rulesets.Mania.UI
columnFlow = new ColumnFlow<Column>(definition)
{
RelativeSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING },
},
new Container
{

View File

@ -193,30 +193,46 @@ namespace osu.Game.Rulesets.Osu
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
new StatisticRow
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
return new[]
{
Columns = new[]
new StatisticRow
{
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList())
Columns = new[]
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
new StatisticItem("Timing Distribution",
new HitEventTimingDistributionGraph(timedHitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
Columns = new[]
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}
}
}
};
};
}
}
}

View File

@ -161,19 +161,34 @@ namespace osu.Game.Rulesets.Taiko
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
new StatisticRow
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
return new[]
{
Columns = new[]
new StatisticRow
{
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList())
Columns = new[]
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}
}
}
};
};
}
}
}

View File

@ -0,0 +1,70 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats
{
[TestFixture]
public class LegacyScoreDecoderTest
{
[Test]
public void TestDecodeManiaReplay()
{
var decoder = new TestLegacyScoreDecoder();
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
{
var score = decoder.Parse(resourceStream);
Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID);
Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]);
Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]);
Assert.AreEqual(829_931, score.ScoreInfo.TotalScore);
Assert.AreEqual(3, score.ScoreInfo.MaxCombo);
Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001));
Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank);
Assert.That(score.Replay.Frames, Is.Not.Empty);
}
}
private class TestLegacyScoreDecoder : LegacyScoreDecoder
{
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
{
new OsuRuleset(),
new TaikoRuleset(),
new CatchRuleset(),
new ManiaRuleset()
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo,
BaseDifficulty = new BeatmapDifficulty()
}
});
}
}
}

Binary file not shown.

View File

@ -0,0 +1,68 @@
// 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 Humanizer;
using NUnit.Framework;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneSimpleStatisticTable : OsuTestScene
{
private Container container;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new Container
{
AutoSizeAxes = Axes.Y,
Width = 700,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333"),
},
container = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(20)
}
}
};
});
[Test]
public void TestEmpty()
{
AddStep("create with no items",
() => container.Add(new SimpleStatisticTable(2, Enumerable.Empty<SimpleStatisticItem>())));
}
[Test]
public void TestManyItems(
[Values(1, 2, 3, 4, 12)] int itemCount,
[Values(1, 3, 5)] int columnCount)
{
AddStep($"create with {"item".ToQuantity(itemCount)}", () =>
{
var items = Enumerable.Range(1, itemCount)
.Select(i => new SimpleStatisticItem<int>($"Statistic #{i}")
{
Value = RNG.Next(100)
});
container.Add(new SimpleStatisticTable(columnCount, items));
});
}
}
}

View File

@ -119,7 +119,7 @@ namespace osu.Game.Online.Chat
case "http":
case "https":
// length > 3 since all these links need another argument to work
if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh"))
if (args.Length > 3 && args[1] == "osu.ppy.sh")
{
switch (args[2])
{

View File

@ -13,7 +13,6 @@ using osu.Game.Replays.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Users;
using SharpCompress.Compressors.LZMA;
@ -123,12 +122,12 @@ namespace osu.Game.Scoring.Legacy
protected void CalculateAccuracy(ScoreInfo score)
{
score.Statistics.TryGetValue(HitResult.Miss, out int countMiss);
score.Statistics.TryGetValue(HitResult.Meh, out int count50);
score.Statistics.TryGetValue(HitResult.Good, out int count100);
score.Statistics.TryGetValue(HitResult.Great, out int count300);
score.Statistics.TryGetValue(HitResult.Perfect, out int countGeki);
score.Statistics.TryGetValue(HitResult.Ok, out int countKatu);
int countMiss = score.GetCountMiss() ?? 0;
int count50 = score.GetCount50() ?? 0;
int count100 = score.GetCount100() ?? 0;
int count300 = score.GetCount300() ?? 0;
int countGeki = score.GetCountGeki() ?? 0;
int countKatu = score.GetCountKatu() ?? 0;
switch (score.Ruleset.ID)
{
@ -241,12 +240,15 @@ namespace osu.Game.Scoring.Legacy
}
var diff = Parsing.ParseFloat(split[0]);
var mouseX = Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE);
var mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE);
lastTime += diff;
if (i == 0 && diff == 0)
// osu-stable adds a zero-time frame before potentially valid negative user frames.
// we need to ignore this.
if (i < 2 && mouseX == 256 && mouseY == -500)
// at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively.
// both frames use a position of (256, -500).
// ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania)
continue;
// Todo: At some point we probably want to rewind and play back the negative-time frames
@ -255,8 +257,8 @@ namespace osu.Game.Scoring.Legacy
continue;
currentFrame = convertFrame(new LegacyReplayFrame(lastTime,
Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE),
Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE),
mouseX,
mouseY,
(ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame);
replay.Frames.Add(currentFrame);

View File

@ -205,6 +205,7 @@ namespace osu.Game.Screens.Menu
const int line_end_offset = 120;
smallRing.Foreground.ResizeTo(1, line_duration, Easing.OutQuint);
smallRing.Delay(400).FadeColour(Color4.Black, 300);
lineTopLeft.MoveTo(new Vector2(-line_end_offset, -line_end_offset), line_duration, Easing.OutQuint);
lineTopRight.MoveTo(new Vector2(line_end_offset, -line_end_offset), line_duration, Easing.OutQuint);

View File

@ -0,0 +1,81 @@
// 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;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Screens.Ranking.Statistics
{
/// <summary>
/// Represents a simple statistic item (one that only needs textual display).
/// Richer visualisations should be done with <see cref="StatisticItem"/>s.
/// </summary>
public abstract class SimpleStatisticItem : Container
{
/// <summary>
/// The text to display as the statistic's value.
/// </summary>
protected string Value
{
set => this.value.Text = value;
}
private readonly OsuSpriteText value;
/// <summary>
/// Creates a new simple statistic item.
/// </summary>
/// <param name="name">The name of the statistic.</param>
protected SimpleStatisticItem(string name)
{
Name = name;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
AddRange(new[]
{
new OsuSpriteText
{
Text = Name,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14)
},
value = new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold)
}
});
}
}
/// <summary>
/// Strongly-typed generic specialisation for <see cref="SimpleStatisticItem"/>.
/// </summary>
public class SimpleStatisticItem<TValue> : SimpleStatisticItem
{
/// <summary>
/// The statistic's value to be displayed.
/// </summary>
public new TValue Value
{
set => base.Value = DisplayValue(value);
}
/// <summary>
/// Used to convert <see cref="Value"/> to a text representation.
/// Defaults to using <see cref="object.ToString"/>.
/// </summary>
protected virtual string DisplayValue(TValue value) => value.ToString();
public SimpleStatisticItem(string name)
: base(name)
{
}
}
}

View File

@ -0,0 +1,123 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Ranking.Statistics
{
/// <summary>
/// Represents a table with simple statistics (ones that only need textual display).
/// Richer visualisations should be done with <see cref="StatisticRow"/>s and <see cref="StatisticItem"/>s.
/// </summary>
public class SimpleStatisticTable : CompositeDrawable
{
private readonly SimpleStatisticItem[] items;
private readonly int columnCount;
private FillFlowContainer[] columns;
/// <summary>
/// Creates a statistic row for the supplied <see cref="SimpleStatisticItem"/>s.
/// </summary>
/// <param name="columnCount">The number of columns to layout the <paramref name="items"/> into.</param>
/// <param name="items">The <see cref="SimpleStatisticItem"/>s to display in this row.</param>
public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable<SimpleStatisticItem> items)
{
if (columnCount < 1)
throw new ArgumentOutOfRangeException(nameof(columnCount));
this.columnCount = columnCount;
this.items = items.ToArray();
}
[BackgroundDependencyLoader]
private void load()
{
columns = new FillFlowContainer[columnCount];
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
ColumnDimensions = createColumnDimensions().ToArray(),
Content = new[] { createColumns().ToArray() }
};
for (int i = 0; i < items.Length; ++i)
columns[i % columnCount].Add(items[i]);
}
private IEnumerable<Dimension> createColumnDimensions()
{
for (int column = 0; column < columnCount; ++column)
{
if (column > 0)
yield return new Dimension(GridSizeMode.Absolute, 30);
yield return new Dimension();
}
}
private IEnumerable<Drawable> createColumns()
{
for (int column = 0; column < columnCount; ++column)
{
if (column > 0)
{
yield return new Spacer
{
Alpha = items.Length > column ? 1 : 0
};
}
yield return columns[column] = createColumn();
}
}
private FillFlowContainer createColumn() => new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical
};
private class Spacer : CompositeDrawable
{
public Spacer()
{
RelativeSizeAxes = Axes.Both;
Padding = new MarginPadding { Vertical = 4 };
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Y,
Width = 3,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
CornerRadius = 2,
Masking = true,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#222")
}
};
}
}
}
}

View File

@ -32,33 +32,9 @@ namespace osu.Game.Screens.Ranking.Statistics
AutoSizeAxes = Axes.Y,
Content = new[]
{
new Drawable[]
new[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
new Circle
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 9,
Width = 4,
Colour = Color4Extensions.FromHex("#00FFAA")
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = item.Name,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
}
}
}
createHeader(item)
},
new Drawable[]
{
@ -78,5 +54,37 @@ namespace osu.Game.Screens.Ranking.Statistics
}
};
}
private static Drawable createHeader(StatisticItem item)
{
if (string.IsNullOrEmpty(item.Name))
return Empty();
return new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
new Circle
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 9,
Width = 4,
Colour = Color4Extensions.FromHex("#00FFAA")
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = item.Name,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
}
}
};
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Screens.Ranking.Statistics
/// <summary>
/// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen.
/// </summary>
/// <param name="name">The name of the item.</param>
/// <param name="name">The name of the item. Can be <see cref="string.Empty"/> to hide the item header.</param>
/// <param name="content">The <see cref="Drawable"/> content to be displayed.</param>
/// <param name="dimension">The <see cref="Dimension"/> of this item. This can be thought of as the column dimension of an encompassing <see cref="GridContainer"/>.</param>
public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null)

View File

@ -94,14 +94,15 @@ namespace osu.Game.Screens.Ranking.Statistics
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(30, 15),
Alpha = 0
};
foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap))
{
rows.Add(new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
@ -125,6 +126,7 @@ namespace osu.Game.Screens.Ranking.Statistics
spinner.Hide();
content.Add(d);
d.FadeIn(250, Easing.OutQuint);
}, localCancellationSource.Token);
}), localCancellationSource.Token);
}

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Ranking.Statistics
{
/// <summary>
/// Displays the unstable rate statistic for a given play.
/// </summary>
public class UnstableRate : SimpleStatisticItem<double>
{
/// <summary>
/// Creates and computes an <see cref="UnstableRate"/> statistic.
/// </summary>
/// <param name="hitEvents">Sequence of <see cref="HitEvent"/>s to calculate the unstable rate based on.</param>
public UnstableRate(IEnumerable<HitEvent> hitEvents)
: base("Unstable Rate")
{
var timeOffsets = hitEvents.Select(ev => ev.TimeOffset).ToArray();
Value = 10 * standardDeviation(timeOffsets);
}
private static double standardDeviation(double[] timeOffsets)
{
if (timeOffsets.Length == 0)
return double.NaN;
var mean = timeOffsets.Average();
var squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum();
return Math.Sqrt(squares / timeOffsets.Length);
}
protected override string DisplayValue(double value) => double.IsNaN(value) ? "(not available)" : value.ToString("N2");
}
}

View File

@ -36,6 +36,7 @@ namespace osu.Game.Skinning
public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR;
public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR;
public bool ShowJudgementLine = true;
public bool KeysUnderNotes;
public LegacyManiaSkinConfiguration(int keys)
{

View File

@ -52,5 +52,6 @@ namespace osu.Game.Skinning
Hit100,
Hit50,
Hit0,
KeysUnderNotes,
}
}

View File

@ -97,6 +97,10 @@ namespace osu.Game.Skinning
currentConfig.ShowJudgementLine = pair.Value == "1";
break;
case "KeysUnderNotes":
currentConfig.KeysUnderNotes = pair.Value == "1";
break;
case "LightingNWidth":
parseArrayValue(pair.Value, currentConfig.ExplosionWidth);
break;

View File

@ -272,6 +272,9 @@ namespace osu.Game.Skinning
case LegacyManiaSkinConfigurationLookups.Hit300:
case LegacyManiaSkinConfigurationLookups.Hit300g:
return SkinUtils.As<TValue>(getManiaImage(existing, maniaLookup.Lookup.ToString()));
case LegacyManiaSkinConfigurationLookups.KeysUnderNotes:
return SkinUtils.As<TValue>(new Bindable<bool>(existing.KeysUnderNotes));
}
return null;