1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 03:13:21 +08:00

Merge branch 'master' into beginplaying-score-token

This commit is contained in:
Bartłomiej Dach 2022-12-13 17:15:24 +01:00 committed by GitHub
commit 849245b90c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 356 additions and 216 deletions

View File

@ -6,9 +6,9 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
public partial class CatchRelaxCursorContainer : GameplayCursorContainer public partial class CatchCursorContainer : GameplayCursorContainer
{ {
// Just hide the cursor in relax. // Just hide the cursor.
// The main goal here is to show that we have a cursor so the game never shows the global one. // The main goal here is to show that we have a cursor so the game never shows the global one.
protected override Drawable CreateCursor() => Empty(); protected override Drawable CreateCursor() => Empty();
} }

View File

@ -3,14 +3,12 @@
#nullable disable #nullable disable
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -52,13 +50,7 @@ namespace osu.Game.Rulesets.Catch.UI
this.difficulty = difficulty; this.difficulty = difficulty;
} }
protected override GameplayCursorContainer CreateCursor() protected override GameplayCursorContainer CreateCursor() => new CatchCursorContainer();
{
if (Mods != null && Mods.Any(m => m is ModRelax))
return new CatchRelaxCursorContainer();
return base.CreateCursor();
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -16,7 +16,9 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
@ -179,6 +181,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
}); });
} }
[Test]
public void TestSoloScoreData()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
scoreInfo.Mods = new Mod[]
{
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
Replay = new Replay
{
Frames = new List<ReplayFrame>
{
new OsuReplayFrame(2000, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
}
}
};
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.Statistics, Is.EqualTo(scoreInfo.Statistics));
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
});
}
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap) private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{ {
var encodeStream = new MemoryStream(); var encodeStream = new MemoryStream();

View File

@ -117,11 +117,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
BeatmapID = 0, BeatmapID = 0,
RulesetID = 0, RulesetID = 0,
Mods = user.Mods, Mods = user.Mods,
MaximumScoringValues = new ScoringValues MaximumStatistics = new Dictionary<HitResult, int>
{ {
BaseScore = 10000, { HitResult.Perfect, 100 }
MaxCombo = 1000,
CountBasicHitObjects = 1000
} }
}; };
} }

View File

@ -0,0 +1,129 @@
// 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.Containers;
using osu.Framework.Graphics;
using osu.Game.Overlays.Settings;
using NUnit.Framework;
using osuTK;
using osu.Game.Overlays;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Allocation;
using osu.Game.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneButtonsInput : OsuManualInputManagerTestScene
{
private const int width = 500;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
private readonly SettingsButton settingsButton;
private readonly OsuClickableContainer clickableContainer;
private readonly RoundedButton roundedButton;
private readonly ShearedButton shearedButton;
public TestSceneButtonsInput()
{
Add(new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Width = 500,
Spacing = new Vector2(0, 5),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
clickableContainer = new OsuClickableContainer
{
RelativeSizeAxes = Axes.X,
Height = 40,
Enabled = { Value = true },
Masking = true,
CornerRadius = 20,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Red
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Rounded clickable container"
}
}
},
settingsButton = new SettingsButton
{
Enabled = { Value = true },
Text = "Settings button"
},
roundedButton = new RoundedButton
{
RelativeSizeAxes = Axes.X,
Enabled = { Value = true },
Text = "Rounded button"
},
shearedButton = new ShearedButton(width)
{
Text = "Sheared button",
LighterColour = Colour4.FromHex("#FFFFFF"),
DarkerColour = Colour4.FromHex("#FFCC22"),
TextColour = Colour4.Black,
Height = 40,
Enabled = { Value = true },
Padding = new MarginPadding(0)
}
}
});
}
[Test]
public void TestSettingsButtonInput()
{
AddStep("Move cursor to button", () => InputManager.MoveMouseTo(settingsButton));
AddAssert("Button is hovered", () => settingsButton.IsHovered);
AddStep("Move cursor to padded area", () => InputManager.MoveMouseTo(settingsButton.ScreenSpaceDrawQuad.TopLeft + new Vector2(SettingsPanel.CONTENT_MARGINS / 2f, 10)));
AddAssert("Cursor within a button", () => settingsButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
AddAssert("Button is not hovered", () => !settingsButton.IsHovered);
}
[Test]
public void TestRoundedButtonInput()
{
AddStep("Move cursor to button", () => InputManager.MoveMouseTo(roundedButton));
AddAssert("Button is hovered", () => roundedButton.IsHovered);
AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(roundedButton.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
AddAssert("Cursor within a button", () => roundedButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
AddAssert("Button is not hovered", () => !roundedButton.IsHovered);
}
[Test]
public void TestShearedButtonInput()
{
AddStep("Move cursor to button", () => InputManager.MoveMouseTo(shearedButton));
AddAssert("Button is hovered", () => shearedButton.IsHovered);
AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(shearedButton.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
AddAssert("Cursor within a button", () => shearedButton.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
AddAssert("Button is not hovered", () => !shearedButton.IsHovered);
}
[Test]
public void TestRoundedClickableContainerInput()
{
AddStep("Move cursor to button", () => InputManager.MoveMouseTo(clickableContainer));
AddAssert("Button is hovered", () => clickableContainer.IsHovered);
AddStep("Move cursor to corner", () => InputManager.MoveMouseTo(clickableContainer.ScreenSpaceDrawQuad.TopLeft + Vector2.One));
AddAssert("Cursor within a button", () => clickableContainer.ScreenSpaceDrawQuad.Contains(InputManager.CurrentState.Mouse.Position));
AddAssert("Button is not hovered", () => !clickableContainer.IsHovered);
}
}
}

View File

@ -1,47 +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.
#nullable disable
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneOsuButton : OsuTestScene
{
[Test]
public void TestToggleEnabled()
{
OsuButton button = null;
AddStep("add button", () => Child = button = new OsuButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
Text = "Button"
});
AddToggleStep("toggle enabled", toggle =>
{
for (int i = 0; i < 6; i++)
button.Action = toggle ? () => { } : null;
});
}
[Test]
public void TestInitiallyDisabled()
{
AddStep("add button", () => Child = new OsuButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
Text = "Button"
});
}
}
}

View File

@ -1,14 +1,13 @@
// 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.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
@ -18,6 +17,12 @@ namespace osu.Game.Graphics.Containers
private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; private readonly Container content = new Container { RelativeSizeAxes = Axes.Both };
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
// base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
base.ReceivePositionalInputAt(screenSpacePos)
// Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
&& Content.ReceivePositionalInputAt(screenSpacePos);
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } }; protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } };
@ -38,11 +43,8 @@ namespace osu.Game.Graphics.Containers
content.AutoSizeAxes = AutoSizeAxes; content.AutoSizeAxes = AutoSizeAxes;
} }
InternalChildren = new Drawable[] AddInternal(content);
{ Add(CreateHoverSounds(sampleSet));
content,
CreateHoverSounds(sampleSet)
};
} }
} }
} }

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -13,6 +11,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
@ -20,16 +19,12 @@ namespace osu.Game.Graphics.UserInterface
/// <summary> /// <summary>
/// A button with added default sound effects. /// A button with added default sound effects.
/// </summary> /// </summary>
public partial class OsuButton : Button public abstract partial class OsuButton : Button
{ {
public LocalisableString Text public LocalisableString Text
{ {
get => SpriteText?.Text ?? default; get => SpriteText.Text;
set set => SpriteText.Text = value;
{
if (SpriteText != null)
SpriteText.Text = value;
}
} }
private Color4? backgroundColour; private Color4? backgroundColour;
@ -66,13 +61,19 @@ namespace osu.Game.Graphics.UserInterface
protected override Container<Drawable> Content { get; } protected override Container<Drawable> Content { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
// base call is checked for cases when `OsuClickableContainer` has masking applied to it directly (ie. externally in object initialisation).
base.ReceivePositionalInputAt(screenSpacePos)
// Implementations often apply masking / edge rounding at a content level, so it's imperative to check that as well.
&& Content.ReceivePositionalInputAt(screenSpacePos);
protected Box Hover; protected Box Hover;
protected Box Background; protected Box Background;
protected SpriteText SpriteText; protected SpriteText SpriteText;
private readonly Box flashLayer; private readonly Box flashLayer;
public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button) protected OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
{ {
Height = 40; Height = 40;
@ -115,7 +116,7 @@ namespace osu.Game.Graphics.UserInterface
}); });
if (hoverSounds.HasValue) if (hoverSounds.HasValue)
AddInternal(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } }); Add(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -175,7 +175,7 @@ namespace osu.Game.Online.Spectator
currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentState.State = SpectatedUserState.Playing; currentState.State = SpectatedUserState.Playing;
currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues; currentState.MaximumStatistics = state.ScoreProcessor.MaximumStatistics;
currentBeatmap = state.Beatmap; currentBeatmap = state.Beatmap;
currentScore = score; currentScore = score;

View File

@ -152,12 +152,12 @@ namespace osu.Game.Online.Spectator
scoreInfo.MaxCombo = frame.Header.MaxCombo; scoreInfo.MaxCombo = frame.Header.MaxCombo;
scoreInfo.Statistics = frame.Header.Statistics; scoreInfo.Statistics = frame.Header.Statistics;
scoreInfo.MaximumStatistics = spectatorState.MaximumStatistics;
Accuracy.Value = frame.Header.Accuracy; Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo; Combo.Value = frame.Header.Combo;
scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _); TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues);
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -9,7 +9,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using MessagePack; using MessagePack;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Online.Spectator namespace osu.Game.Online.Spectator
{ {
@ -31,7 +31,7 @@ namespace osu.Game.Online.Spectator
public SpectatedUserState State { get; set; } public SpectatedUserState State { get; set; }
[Key(4)] [Key(4)]
public ScoringValues MaximumScoringValues { get; set; } public Dictionary<HitResult, int> MaximumStatistics { get; set; } = new Dictionary<HitResult, int>();
public bool Equals(SpectatorState other) public bool Equals(SpectatorState other)
{ {

View File

@ -68,11 +68,15 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 20 }, Margin = new MarginPadding { Top = 20 },
Children = new Drawable[] Child = new FillFlowContainer
{ {
new OsuHoverContainer Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Child = new OsuHoverContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -4,7 +4,6 @@
#nullable disable #nullable disable
using System; using System;
using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -104,27 +103,29 @@ namespace osu.Game.Overlays.Changelog
{ {
var fill = base.CreateHeader(); var fill = base.CreateHeader();
foreach (var existing in fill.Children.OfType<OsuHoverContainer>()) var nestedFill = (FillFlowContainer)fill.Child;
{
existing.Scale = new Vector2(1.25f);
existing.Action = null;
existing.Add(date = new OsuSpriteText var buildDisplay = (OsuHoverContainer)nestedFill.Child;
buildDisplay.Scale = new Vector2(1.25f);
buildDisplay.Action = null;
fill.Add(date = new OsuSpriteText
{ {
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = Build.CreatedAt.Date.ToString("dd MMMM yyyy"), Text = Build.CreatedAt.Date.ToString("dd MMMM yyyy"),
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14), Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 14),
Anchor = Anchor.BottomCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = 5 }, Margin = new MarginPadding { Top = 5 },
Scale = new Vector2(1.25f),
}); });
}
fill.Insert(-1, new NavigationIconButton(Build.Versions?.Previous) nestedFill.Insert(-1, new NavigationIconButton(Build.Versions?.Previous)
{ {
Icon = FontAwesome.Solid.ChevronLeft, Icon = FontAwesome.Solid.ChevronLeft,
SelectBuild = b => SelectBuild(b) SelectBuild = b => SelectBuild(b)
}); });
fill.Insert(1, new NavigationIconButton(Build.Versions?.Next) nestedFill.Insert(1, new NavigationIconButton(Build.Versions?.Next)
{ {
Icon = FontAwesome.Solid.ChevronRight, Icon = FontAwesome.Solid.ChevronRight,
SelectBuild = b => SelectBuild(b) SelectBuild = b => SelectBuild(b)

View File

@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Chat
public Color4 AccentColour { get; } public Color4 AccentColour { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
Child.ReceivePositionalInputAt(screenSpacePos); colouredDrawable.ReceivePositionalInputAt(screenSpacePos);
public float FontSize public float FontSize
{ {
@ -87,13 +87,13 @@ namespace osu.Game.Overlays.Chat
{ {
AccentColour = default_colours[user.Id % default_colours.Length]; AccentColour = default_colours[user.Id % default_colours.Length];
Child = colouredDrawable = drawableText; Add(colouredDrawable = drawableText);
} }
else else
{ {
AccentColour = Color4Extensions.FromHex(user.Colour); AccentColour = Color4Extensions.FromHex(user.Colour);
Child = new Container Add(new Container
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
@ -127,7 +127,7 @@ namespace osu.Game.Overlays.Chat
} }
} }
} }
}; });
} }
} }

View File

@ -5,9 +5,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Extensions;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
@ -67,7 +69,7 @@ namespace osu.Game.Rulesets.Configuration
{ {
var setting = r.All<RealmRulesetSetting>().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString()); var setting = r.All<RealmRulesetSetting>().First(s => s.RulesetName == rulesetName && s.Variant == variant && s.Key == c.ToString());
setting.Value = ConfigStore[c].ToString(); setting.Value = ConfigStore[c].ToString(CultureInfo.InvariantCulture);
} }
}); });
@ -89,7 +91,7 @@ namespace osu.Game.Rulesets.Configuration
setting = new RealmRulesetSetting setting = new RealmRulesetSetting
{ {
Key = lookup.ToString(), Key = lookup.ToString(),
Value = bindable.Value.ToString(), Value = bindable.ToString(CultureInfo.InvariantCulture),
RulesetName = rulesetName, RulesetName = rulesetName,
Variant = variant, Variant = variant,
}; };

View File

@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -90,17 +89,14 @@ namespace osu.Game.Rulesets.Scoring
private readonly double accuracyPortion; private readonly double accuracyPortion;
private readonly double comboPortion; private readonly double comboPortion;
/// <summary> public Dictionary<HitResult, int> MaximumStatistics
/// Scoring values for a perfect play.
/// </summary>
public ScoringValues MaximumScoringValues
{ {
get get
{ {
if (!beatmapApplied) if (!beatmapApplied)
throw new InvalidOperationException($"Cannot access maximum scoring values before calling {nameof(ApplyBeatmap)}."); throw new InvalidOperationException($"Cannot access maximum statistics before calling {nameof(ApplyBeatmap)}.");
return maximumScoringValues; return new Dictionary<HitResult, int>(maximumResultCounts);
} }
} }
@ -268,7 +264,7 @@ namespace osu.Game.Rulesets.Scoring
private void updateScore() private void updateScore()
{ {
Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1; Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues); TotalScore.Value = computeScore(Mode.Value, currentScoringValues, maximumScoringValues);
} }
/// <summary> /// <summary>
@ -303,9 +299,9 @@ namespace osu.Game.Rulesets.Scoring
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
ExtractScoringValues(scoreInfo, out var current, out var maximum); extractScoringValues(scoreInfo, out var current, out var maximum);
return ComputeScore(mode, current, maximum); return computeScore(mode, current, maximum);
} }
/// <summary> /// <summary>
@ -316,7 +312,7 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="maximum">The maximum scoring values.</param> /// <param name="maximum">The maximum scoring values.</param>
/// <returns>The total score computed from the given scoring values.</returns> /// <returns>The total score computed from the given scoring values.</returns>
[Pure] [Pure]
public long ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum) private long computeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
{ {
double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1; double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1; double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
@ -474,14 +470,14 @@ namespace osu.Game.Rulesets.Scoring
/// Consumers are expected to more accurately fill in the above values through external means. /// Consumers are expected to more accurately fill in the above values through external means.
/// <para> /// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in /// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
/// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>. /// <see cref="computeScore(osu.Game.Rulesets.Scoring.ScoringMode,ScoringValues,ScoringValues)"/>.
/// </para> /// </para>
/// </remarks> /// </remarks>
/// <param name="scoreInfo">The score to extract scoring values from.</param> /// <param name="scoreInfo">The score to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param> /// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param> /// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure] [Pure]
internal void ExtractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum) private void extractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
{ {
extractScoringValues(scoreInfo.Statistics, out current, out maximum); extractScoringValues(scoreInfo.Statistics, out current, out maximum);
current.MaxCombo = scoreInfo.MaxCombo; current.MaxCombo = scoreInfo.MaxCombo;
@ -490,31 +486,6 @@ namespace osu.Game.Rulesets.Scoring
extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum); extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum);
} }
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
/// <remarks>
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet">
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list>
/// Consumers are expected to more accurately fill in the above values through external means.
/// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
/// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>.
/// </para>
/// </remarks>
/// <param name="header">The replay frame header to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
internal void ExtractScoringValues(FrameHeader header, out ScoringValues current, out ScoringValues maximum)
{
extractScoringValues(header.Statistics, out current, out maximum);
current.MaxCombo = header.MaxCombo;
}
/// <summary> /// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>. /// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary> /// </summary>
@ -589,6 +560,32 @@ namespace osu.Game.Rulesets.Scoring
base.Dispose(isDisposing); base.Dispose(isDisposing);
hitEvents.Clear(); hitEvents.Clear();
} }
/// <summary>
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
/// </summary>
private struct ScoringValues
{
/// <summary>
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
public long BaseScore;
/// <summary>
/// The sum of all "bonus" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBonus"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
public long BonusScore;
/// <summary>
/// The highest achieved combo.
/// </summary>
public int MaxCombo;
/// <summary>
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
/// </summary>
public int CountBasicHitObjects;
}
} }
public enum ScoringMode public enum ScoringMode

View File

@ -0,0 +1,38 @@
// 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 Newtonsoft.Json;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring.Legacy
{
/// <summary>
/// A minified version of <see cref="SoloScoreInfo"/> retrofit onto the end of legacy replay files (.osr),
/// containing the minimum data required to support storage of non-legacy replays.
/// </summary>
[Serializable]
[JsonObject(MemberSerialization.OptIn)]
public class LegacyReplaySoloScoreInfo
{
[JsonProperty("mods")]
public APIMod[] Mods { get; set; } = Array.Empty<APIMod>();
[JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
[JsonProperty("maximum_statistics")]
public Dictionary<HitResult, int> MaximumStatistics { get; set; } = new Dictionary<HitResult, int>();
public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo
{
Mods = score.APIMods,
Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
};
}
}

View File

@ -4,8 +4,10 @@
#nullable disable #nullable disable
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
@ -91,9 +93,41 @@ namespace osu.Game.Scoring.Legacy
else if (version >= 20121008) else if (version >= 20121008)
scoreInfo.OnlineID = sr.ReadInt32(); scoreInfo.OnlineID = sr.ReadInt32();
byte[] compressedScoreInfo = null;
if (version >= 30000001)
compressedScoreInfo = sr.ReadByteArray();
if (compressedReplay?.Length > 0) if (compressedReplay?.Length > 0)
readCompressedData(compressedReplay, reader => readLegacyReplay(score.Replay, reader));
if (compressedScoreInfo?.Length > 0)
{ {
using (var replayInStream = new MemoryStream(compressedReplay)) readCompressedData(compressedScoreInfo, reader =>
{
LegacyReplaySoloScoreInfo readScore = JsonConvert.DeserializeObject<LegacyReplaySoloScoreInfo>(reader.ReadToEnd());
Debug.Assert(readScore != null);
score.ScoreInfo.Statistics = readScore.Statistics;
score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics;
score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray();
});
}
}
PopulateAccuracy(score.ScoreInfo);
// before returning for database import, we must restore the database-sourced BeatmapInfo.
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo;
return score;
}
private void readCompressedData(byte[] data, Action<StreamReader> readFunc)
{
using (var replayInStream = new MemoryStream(data))
{ {
byte[] properties = new byte[5]; byte[] properties = new byte[5];
if (replayInStream.Read(properties, 0, 5) != 5) if (replayInStream.Read(properties, 0, 5) != 5)
@ -114,19 +148,9 @@ namespace osu.Game.Scoring.Legacy
using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize)) using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize))
using (var reader = new StreamReader(lzma)) using (var reader = new StreamReader(lzma))
readLegacyReplay(score.Replay, reader); readFunc(reader);
} }
} }
}
PopulateAccuracy(score.ScoreInfo);
// before returning for database import, we must restore the database-sourced BeatmapInfo.
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo;
return score;
}
/// <summary> /// <summary>
/// Populates the accuracy of a given <see cref="ScoreInfo"/> from its contained statistics. /// Populates the accuracy of a given <see cref="ScoreInfo"/> from its contained statistics.

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.IO.Legacy; using osu.Game.IO.Legacy;
using osu.Game.IO.Serialization;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
@ -24,7 +25,7 @@ namespace osu.Game.Scoring.Legacy
/// Database version in stable-compatible YYYYMMDD format. /// Database version in stable-compatible YYYYMMDD format.
/// Should be incremented if any changes are made to the format/usage. /// Should be incremented if any changes are made to the format/usage.
/// </summary> /// </summary>
public const int LATEST_VERSION = FIRST_LAZER_VERSION; public const int LATEST_VERSION = 30000001;
/// <summary> /// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays. /// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
@ -52,9 +53,9 @@ namespace osu.Game.Scoring.Legacy
throw new ArgumentException(@"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); throw new ArgumentException(@"Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score));
} }
public void Encode(Stream stream) public void Encode(Stream stream, bool leaveOpen = false)
{ {
using (SerializationWriter sw = new SerializationWriter(stream)) using (SerializationWriter sw = new SerializationWriter(stream, leaveOpen))
{ {
sw.Write((byte)(score.ScoreInfo.Ruleset.OnlineID)); sw.Write((byte)(score.ScoreInfo.Ruleset.OnlineID));
sw.Write(LATEST_VERSION); sw.Write(LATEST_VERSION);
@ -77,6 +78,7 @@ namespace osu.Game.Scoring.Legacy
sw.WriteByteArray(createReplayData()); sw.WriteByteArray(createReplayData());
sw.Write((long)0); sw.Write((long)0);
writeModSpecificData(score.ScoreInfo, sw); writeModSpecificData(score.ScoreInfo, sw);
sw.WriteByteArray(createScoreInfoData());
} }
} }
@ -84,9 +86,13 @@ namespace osu.Game.Scoring.Legacy
{ {
} }
private byte[] createReplayData() private byte[] createReplayData() => compress(replayStringContent);
private byte[] createScoreInfoData() => compress(LegacyReplaySoloScoreInfo.FromScore(score.ScoreInfo).Serialize());
private byte[] compress(string data)
{ {
byte[] content = new ASCIIEncoding().GetBytes(replayStringContent); byte[] content = new ASCIIEncoding().GetBytes(data);
using (var outStream = new MemoryStream()) using (var outStream = new MemoryStream())
{ {

View File

@ -1,43 +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.
#nullable disable
using MessagePack;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring
{
/// <summary>
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
/// </summary>
[MessagePackObject]
public struct ScoringValues
{
/// <summary>
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
[Key(0)]
public long BaseScore;
/// <summary>
/// The sum of all "bonus" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBonus"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
[Key(1)]
public long BonusScore;
/// <summary>
/// The highest achieved combo.
/// </summary>
[Key(2)]
public int MaxCombo;
/// <summary>
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
/// </summary>
[Key(3)]
public int CountBasicHitObjects;
}
}