diff --git a/osu.Android.props b/osu.Android.props
index c28085557e..116c7dbfcd 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 23500f5da6..79ff222a89 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
if (positionInfo == positionInfos.First())
{
- positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
+ positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
}
else
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
index da73c2addb..266f7d1251 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
@@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Utils
if (!(osuObject is Slider slider))
return;
+ // No need to update the head and tail circles, since slider handles that when the new slider path is set
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
@@ -137,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Utils
if (!(osuObject is Slider slider))
return;
+ // No need to update the head and tail circles, since slider handles that when the new slider path is set
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
@@ -146,5 +148,41 @@ namespace osu.Game.Rulesets.Osu.Utils
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
}
+
+ ///
+ /// Rotate a slider about its start position by the specified angle.
+ ///
+ /// The slider to be rotated.
+ /// The angle, measured in radians, to rotate the slider by.
+ public static void RotateSlider(Slider slider, float rotation)
+ {
+ void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
+
+ // No need to update the head and tail circles, since slider handles that when the new slider path is set
+ slider.NestedHitObjects.OfType().ForEach(rotateNestedObject);
+ slider.NestedHitObjects.OfType().ForEach(rotateNestedObject);
+
+ var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
+ foreach (var point in controlPoints)
+ point.Position = rotateVector(point.Position, rotation);
+
+ slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
+ }
+
+ ///
+ /// Rotate a vector by the specified angle.
+ ///
+ /// The vector to be rotated.
+ /// The angle, measured in radians, to rotate the vector by.
+ /// The rotated vector.
+ private static Vector2 rotateVector(Vector2 vector, float rotation)
+ {
+ float angle = MathF.Atan2(vector.Y, vector.X) + rotation;
+ float length = vector.Length;
+ return new Vector2(
+ length * MathF.Cos(angle),
+ length * MathF.Sin(angle)
+ );
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
index d1bc3b45df..a77d1f8b0f 100644
--- a/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
+++ b/osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils_Reposition.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Primitives;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
@@ -37,15 +38,23 @@ namespace osu.Game.Rulesets.Osu.Utils
foreach (OsuHitObject hitObject in hitObjects)
{
Vector2 relativePosition = hitObject.Position - previousPosition;
- float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ float absoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
float relativeAngle = absoluteAngle - previousAngle;
- positionInfos.Add(new ObjectPositionInfo(hitObject)
+ ObjectPositionInfo positionInfo;
+ positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject)
{
RelativeAngle = relativeAngle,
DistanceFromPrevious = relativePosition.Length
});
+ if (hitObject is Slider slider)
+ {
+ float absoluteRotation = getSliderRotation(slider);
+ positionInfo.Rotation = absoluteRotation - absoluteAngle;
+ absoluteAngle = absoluteRotation;
+ }
+
previousPosition = hitObject.EndPosition;
previousAngle = absoluteAngle;
}
@@ -70,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Utils
if (hitObject is Spinner)
{
- previous = null;
+ previous = current;
continue;
}
@@ -124,16 +133,23 @@ namespace osu.Game.Rulesets.Osu.Utils
if (previous != null)
{
- Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
- Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
- previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
+ if (previous.HitObject is Slider s)
+ {
+ previousAbsoluteAngle = getSliderRotation(s);
+ }
+ else
+ {
+ Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
+ Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
+ previousAbsoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
+ }
}
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
var posRelativeToPrev = new Vector2(
- current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
- current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
+ current.PositionInfo.DistanceFromPrevious * MathF.Cos(absoluteAngle),
+ current.PositionInfo.DistanceFromPrevious * MathF.Sin(absoluteAngle)
);
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
@@ -141,6 +157,19 @@ namespace osu.Game.Rulesets.Osu.Utils
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
current.PositionModified = lastEndPosition + posRelativeToPrev;
+
+ if (!(current.HitObject is Slider slider))
+ return;
+
+ absoluteAngle = MathF.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
+
+ Vector2 centreOfMassOriginal = calculateCentreOfMass(slider);
+ Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation + absoluteAngle - getSliderRotation(slider));
+ centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified);
+
+ float relativeRotation = MathF.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - MathF.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X);
+ if (!Precision.AlmostEquals(relativeRotation, 0))
+ RotateSlider(slider, relativeRotation);
}
///
@@ -172,13 +201,13 @@ namespace osu.Game.Rulesets.Osu.Utils
var previousPosition = workingObject.PositionModified;
// Clamp slider position to the placement area
- // If the slider is larger than the playfield, force it to stay at the original position
+ // If the slider is larger than the playfield, at least make sure that the head circle is inside the playfield
float newX = possibleMovementBounds.Width < 0
- ? workingObject.PositionOriginal.X
+ ? Math.Clamp(possibleMovementBounds.Left, 0, OsuPlayfield.BASE_SIZE.X)
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
float newY = possibleMovementBounds.Height < 0
- ? workingObject.PositionOriginal.Y
+ ? Math.Clamp(possibleMovementBounds.Top, 0, OsuPlayfield.BASE_SIZE.Y)
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
@@ -287,6 +316,45 @@ namespace osu.Game.Rulesets.Osu.Utils
);
}
+ ///
+ /// Estimate the centre of mass of a slider relative to its start position.
+ ///
+ /// The slider to process.
+ /// The centre of mass of the slider.
+ private static Vector2 calculateCentreOfMass(Slider slider)
+ {
+ const double sample_step = 50;
+
+ // just sample the start and end positions if the slider is too short
+ if (slider.Distance <= sample_step)
+ {
+ return Vector2.Divide(slider.Path.PositionAt(1), 2);
+ }
+
+ int count = 0;
+ Vector2 sum = Vector2.Zero;
+ double pathDistance = slider.Distance;
+
+ for (double i = 0; i < pathDistance; i += sample_step)
+ {
+ sum += slider.Path.PositionAt(i / pathDistance);
+ count++;
+ }
+
+ return sum / count;
+ }
+
+ ///
+ /// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path.
+ ///
+ /// The slider to process.
+ /// The angle in radians.
+ private static float getSliderRotation(Slider slider)
+ {
+ var endPositionVector = slider.Path.PositionAt(1);
+ return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
+ }
+
public class ObjectPositionInfo
{
///
@@ -309,6 +377,13 @@ namespace osu.Game.Rulesets.Osu.Utils
///
public float DistanceFromPrevious { get; set; }
+ ///
+ /// The rotation of the hit object, relative to its jump angle.
+ /// For sliders, this is defined as the angle from the slider's start position to the end of its path, relative to its jump angle.
+ /// For hit circles and spinners, this property is ignored.
+ ///
+ public float Rotation { get; set; }
+
///
/// The hit object associated with this .
///
diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
index 9c307341bd..af4b002bc9 100644
--- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs
@@ -61,13 +61,13 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// No header shouldn't cause any change
- scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame());
+ scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame());
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// Reset with a miss instead.
- scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
+ scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
});
@@ -76,7 +76,7 @@ namespace osu.Game.Tests.Gameplay
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
// Reset with no judged hit.
- scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
+ scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary(), DateTimeOffset.Now)
});
diff --git a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
index 62cea378e6..5e125c1a62 100644
--- a/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/MultiplayerGameplayLeaderboardTestScene.cs
@@ -115,9 +115,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
BeatmapID = 0,
RulesetID = 0,
Mods = user.Mods,
- MaxAchievableCombo = 1000,
- MaxAchievableBaseScore = 10000,
- TotalBasicHitObjects = 1000
+ MaximumScoringValues = new ScoringValues
+ {
+ BaseScore = 10000,
+ MaxCombo = 1000,
+ HitObjects = 1000
+ }
};
}
});
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
index 35a4f8cf2d..edee26c081 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
@@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator;
+using osu.Game.Overlays;
using osu.Game.Overlays.Dashboard;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
@@ -42,7 +43,8 @@ namespace osu.Game.Tests.Visual.Online
CachedDependencies = new (Type, object)[]
{
(typeof(SpectatorClient), spectatorClient),
- (typeof(UserLookupCache), lookupCache)
+ (typeof(UserLookupCache), lookupCache),
+ (typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)),
},
Child = currentlyPlaying = new CurrentlyPlayingDisplay
{
diff --git a/osu.Game/IO/Archives/ZipArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs
index 80dfa104f3..ae2b85da51 100644
--- a/osu.Game/IO/Archives/ZipArchiveReader.cs
+++ b/osu.Game/IO/Archives/ZipArchiveReader.cs
@@ -1,11 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using Microsoft.Toolkit.HighPerformance;
+using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip;
+using SixLabors.ImageSharp.Memory;
namespace osu.Game.IO.Archives
{
@@ -27,15 +31,12 @@ namespace osu.Game.IO.Archives
if (entry == null)
throw new FileNotFoundException();
- // allow seeking
- MemoryStream copy = new MemoryStream();
+ var owner = MemoryAllocator.Default.Allocate((int)entry.Size);
using (Stream s = entry.OpenEntryStream())
- s.CopyTo(copy);
+ s.ReadToFill(owner.Memory.Span);
- copy.Position = 0;
-
- return copy;
+ return new MemoryOwnerMemoryStream(owner);
}
public override void Dispose()
@@ -45,5 +46,48 @@ namespace osu.Game.IO.Archives
}
public override IEnumerable Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
+
+ private class MemoryOwnerMemoryStream : Stream
+ {
+ private readonly IMemoryOwner owner;
+ private readonly Stream stream;
+
+ public MemoryOwnerMemoryStream(IMemoryOwner owner)
+ {
+ this.owner = owner;
+
+ stream = owner.Memory.AsStream();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ owner?.Dispose();
+ base.Dispose(disposing);
+ }
+
+ public override void Flush() => stream.Flush();
+
+ public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count);
+
+ public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin);
+
+ public override void SetLength(long value) => stream.SetLength(value);
+
+ public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count);
+
+ public override bool CanRead => stream.CanRead;
+
+ public override bool CanSeek => stream.CanSeek;
+
+ public override bool CanWrite => stream.CanWrite;
+
+ public override long Length => stream.Length;
+
+ public override long Position
+ {
+ get => stream.Position;
+ set => stream.Position = value;
+ }
+ }
}
}
diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs
index 527f6d06ce..9bf49364f3 100644
--- a/osu.Game/Online/Spectator/SpectatorClient.cs
+++ b/osu.Game/Online/Spectator/SpectatorClient.cs
@@ -172,9 +172,7 @@ namespace osu.Game.Online.Spectator
currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentState.State = SpectatedUserState.Playing;
- currentState.MaxAchievableCombo = state.ScoreProcessor.MaxAchievableCombo;
- currentState.MaxAchievableBaseScore = state.ScoreProcessor.MaxAchievableBaseScore;
- currentState.TotalBasicHitObjects = state.ScoreProcessor.TotalBasicHitObjects;
+ currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues;
currentBeatmap = state.Beatmap;
currentScore = score;
diff --git a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
index 99596fa1d3..e81cf433a5 100644
--- a/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
+++ b/osu.Game/Online/Spectator/SpectatorScoreProcessor.cs
@@ -62,6 +62,7 @@ namespace osu.Game.Online.Spectator
private readonly List replayFrames = new List();
private readonly int userId;
+ private SpectatorState? spectatorState;
private ScoreProcessor? scoreProcessor;
private ScoreInfo? scoreInfo;
@@ -89,6 +90,7 @@ namespace osu.Game.Online.Spectator
scoreProcessor?.RemoveAndDisposeImmediately();
scoreProcessor = null;
scoreInfo = null;
+ spectatorState = null;
replayFrames.Clear();
return;
}
@@ -104,18 +106,13 @@ namespace osu.Game.Online.Spectator
Ruleset ruleset = rulesetInfo.CreateInstance();
+ spectatorState = userState;
scoreInfo = new ScoreInfo { Ruleset = rulesetInfo };
scoreProcessor = ruleset.CreateScoreProcessor();
// Mods are required for score multiplier.
scoreProcessor.Mods.Value = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray();
-
- // Applying beatmap required to call ComputePartialScore().
scoreProcessor.ApplyBeatmap(new DummyBeatmap());
-
- scoreProcessor.MaxAchievableCombo = userState.MaxAchievableCombo;
- scoreProcessor.MaxAchievableBaseScore = userState.MaxAchievableBaseScore;
- scoreProcessor.TotalBasicHitObjects = userState.TotalBasicHitObjects;
}
private void onNewFrames(int incomingUserId, FrameDataBundle bundle)
@@ -138,6 +135,7 @@ namespace osu.Game.Online.Spectator
if (scoreInfo == null || replayFrames.Count == 0)
return;
+ Debug.Assert(spectatorState != null);
Debug.Assert(scoreProcessor != null);
int frameIndex = replayFrames.BinarySearch(new TimedFrame(Time.Current));
@@ -153,7 +151,9 @@ namespace osu.Game.Online.Spectator
Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo;
- TotalScore.Value = scoreProcessor.ComputePartialScore(Mode.Value, scoreInfo);
+
+ scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _);
+ TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs
index 8b2e90ead0..64e5f8b3a1 100644
--- a/osu.Game/Online/Spectator/SpectatorState.cs
+++ b/osu.Game/Online/Spectator/SpectatorState.cs
@@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MessagePack;
using osu.Game.Online.API;
+using osu.Game.Scoring;
namespace osu.Game.Online.Spectator
{
@@ -27,23 +28,8 @@ namespace osu.Game.Online.Spectator
[Key(3)]
public SpectatedUserState State { get; set; }
- ///
- /// The maximum achievable combo, if everything is hit perfectly.
- ///
[Key(4)]
- public int MaxAchievableCombo { get; set; }
-
- ///
- /// The maximum achievable base score, if everything is hit perfectly.
- ///
- [Key(5)]
- public double MaxAchievableBaseScore { get; set; }
-
- ///
- /// The total number of basic (non-tick and non-bonus) hitobjects that can be hit.
- ///
- [Key(6)]
- public int TotalBasicHitObjects { get; set; }
+ public ScoringValues MaximumScoringValues { get; set; }
public bool Equals(SpectatorState other)
{
diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
index a9312e9a3a..23f67a06cb 100644
--- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
@@ -9,11 +10,16 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Game.Database;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator;
+using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play;
@@ -24,26 +30,62 @@ namespace osu.Game.Overlays.Dashboard
{
internal class CurrentlyPlayingDisplay : CompositeDrawable
{
+ private const float search_textbox_height = 40;
+ private const float padding = 10;
+
private readonly IBindableList playingUsers = new BindableList();
- private FillFlowContainer userFlow;
+ private SearchContainer userFlow;
+ private BasicSearchTextBox searchTextBox;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
[BackgroundDependencyLoader]
- private void load()
+ private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- InternalChild = userFlow = new FillFlowContainer
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding(10),
- Spacing = new Vector2(10),
+ new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = padding * 2 + search_textbox_height,
+ Colour = colourProvider.Background4,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Padding = new MarginPadding(padding),
+ Child = searchTextBox = new BasicSearchTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Height = search_textbox_height,
+ ReleaseFocusOnCommit = false,
+ HoldFocus = true,
+ PlaceholderText = HomeStrings.SearchPlaceholder,
+ },
+ },
+ userFlow = new SearchContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(10),
+ Padding = new MarginPadding
+ {
+ Top = padding * 3 + search_textbox_height,
+ Bottom = padding,
+ Right = padding,
+ Left = padding,
+ },
+ },
};
+
+ searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue;
}
[Resolved]
@@ -57,6 +99,13 @@ namespace osu.Game.Overlays.Dashboard
playingUsers.BindCollectionChanged(onPlayingUsersChanged, true);
}
+ protected override void OnFocus(FocusEvent e)
+ {
+ base.OnFocus(e);
+
+ searchTextBox.TakeFocus();
+ }
+
private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
{
switch (e.Action)
@@ -102,17 +151,34 @@ namespace osu.Game.Overlays.Dashboard
panel.Origin = Anchor.TopCentre;
});
- private class PlayingUserPanel : CompositeDrawable
+ public class PlayingUserPanel : CompositeDrawable, IFilterable
{
public readonly APIUser User;
+ public IEnumerable FilterTerms { get; }
+
[Resolved(canBeNull: true)]
private IPerformFromScreenRunner performer { get; set; }
+ public bool FilteringActive { set; get; }
+
+ public bool MatchingFilter
+ {
+ set
+ {
+ if (value)
+ Show();
+ else
+ Hide();
+ }
+ }
+
public PlayingUserPanel(APIUser user)
{
User = user;
+ FilterTerms = new LocalisableString[] { User.Username };
+
AutoSizeAxes = Axes.Both;
}
diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs
index 83ad8faf1c..79d972bdcc 100644
--- a/osu.Game/Overlays/DashboardOverlay.cs
+++ b/osu.Game/Overlays/DashboardOverlay.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Overlays
protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader();
+ public override bool AcceptsFocus => false;
+
protected override void CreateDisplayToLoad(DashboardOverlayTabs tab)
{
switch (tab)
diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
index 94ddc32bb7..bfa67b8c45 100644
--- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs
@@ -117,9 +117,8 @@ namespace osu.Game.Rulesets.Scoring
///
/// If the provided replay frame does not have any header information, this will be a noop.
///
- /// The ruleset to be used for retrieving statistics.
/// The replay frame to read header statistics from.
- public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
+ public virtual void ResetFromReplayFrame(ReplayFrame frame)
{
if (frame.Header == null)
return;
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index ec09bfcfa3..8e69ffa0a0 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
+using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -89,19 +90,22 @@ namespace osu.Game.Rulesets.Scoring
private readonly double comboPortion;
///
- /// The maximum achievable combo, if everything is hit perfectly.
+ /// Scoring values for a perfect play.
///
- internal int MaxAchievableCombo;
+ public ScoringValues MaximumScoringValues { get; private set; }
///
- /// The maximum achievable base score, if everything is hit perfectly.
+ /// Scoring values for the current play assuming all perfect hits.
///
- internal double MaxAchievableBaseScore;
+ ///
+ /// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session.
+ ///
+ private ScoringValues currentMaximumScoringValues;
///
- /// The total number of basic (non-tick and non-bonus) hitobjects that can be hit.
+ /// Scoring values for the current play.
///
- internal int TotalBasicHitObjects;
+ private ScoringValues currentScoringValues;
///
/// The maximum of a basic (non-tick and non-bonus) hitobject.
@@ -109,9 +113,6 @@ namespace osu.Game.Rulesets.Scoring
///
private HitResult? maxBasicResult;
- private double rollingMaxAchievableBaseScore;
- private double rollingBaseScore;
- private int rollingBasicHitObjects;
private bool beatmapApplied;
private readonly Dictionary scoreResultCounts = new Dictionary();
@@ -167,23 +168,42 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
if (!result.Type.IsScorable())
- return;
+ {
+ // The inverse of non-scorable (ignore) judgements may be bonus judgements.
+ if (result.Judgement.MaxResult.IsBonus())
+ currentMaximumScoringValues.BonusScore += result.Judgement.MaxNumericResult;
+ return;
+ }
+
+ // Update rolling combo.
if (result.Type.IncreasesCombo())
Combo.Value++;
else if (result.Type.BreaksCombo())
Combo.Value = 0;
- double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
+ // Update maximum combo.
+ currentScoringValues.MaxCombo = HighestCombo.Value;
+ currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.AffectsCombo() ? 1 : 0;
- if (!result.Type.IsBonus())
+ // Update base/bonus score.
+ if (result.Type.IsBonus())
{
- rollingBaseScore += scoreIncrease;
- rollingMaxAchievableBaseScore += result.Judgement.MaxNumericResult;
+ currentScoringValues.BonusScore += result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
+ currentMaximumScoringValues.BonusScore += result.Judgement.MaxNumericResult;
+ }
+ else
+ {
+ currentScoringValues.BaseScore += result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
+ currentMaximumScoringValues.BaseScore += result.Judgement.MaxNumericResult;
}
+ // Update hitobject count.
if (result.Type.IsBasic())
- rollingBasicHitObjects++;
+ {
+ currentScoringValues.HitObjects++;
+ currentMaximumScoringValues.HitObjects++;
+ }
hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject;
@@ -210,18 +230,36 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
if (!result.Type.IsScorable())
- return;
-
- double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
-
- if (!result.Type.IsBonus())
{
- rollingBaseScore -= scoreIncrease;
- rollingMaxAchievableBaseScore -= result.Judgement.MaxNumericResult;
+ // The inverse of non-scorable (ignore) judgements may be bonus judgements.
+ if (result.Judgement.MaxResult.IsBonus())
+ currentMaximumScoringValues.BonusScore -= result.Judgement.MaxNumericResult;
+
+ return;
}
+ // Update maximum combo.
+ currentScoringValues.MaxCombo = HighestCombo.Value;
+ currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.AffectsCombo() ? 1 : 0;
+
+ // Update base/bonus score.
+ if (result.Type.IsBonus())
+ {
+ currentScoringValues.BonusScore -= result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
+ currentMaximumScoringValues.BonusScore -= result.Judgement.MaxNumericResult;
+ }
+ else
+ {
+ currentScoringValues.BaseScore -= result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
+ currentMaximumScoringValues.BaseScore -= result.Judgement.MaxNumericResult;
+ }
+
+ // Update hitobject count.
if (result.Type.IsBasic())
- rollingBasicHitObjects--;
+ {
+ currentScoringValues.HitObjects--;
+ currentMaximumScoringValues.HitObjects--;
+ }
Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject;
@@ -232,12 +270,8 @@ namespace osu.Game.Rulesets.Scoring
private void updateScore()
{
- double rollingAccuracyRatio = rollingMaxAchievableBaseScore > 0 ? rollingBaseScore / rollingMaxAchievableBaseScore : 1;
- double accuracyRatio = MaxAchievableBaseScore > 0 ? rollingBaseScore / MaxAchievableBaseScore : 1;
- double comboRatio = MaxAchievableCombo > 0 ? (double)HighestCombo.Value / MaxAchievableCombo : 1;
-
- Accuracy.Value = rollingAccuracyRatio;
- TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), TotalBasicHitObjects);
+ Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
+ TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, MaximumScoringValues);
}
///
@@ -254,17 +288,10 @@ namespace osu.Game.Rulesets.Scoring
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
- extractFromStatistics(ruleset,
- scoreInfo.Statistics,
- out double extractedBaseScore,
- out double extractedMaxBaseScore,
- out int extractedMaxCombo,
- out int extractedBasicHitObjects);
+ extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
+ current.MaxCombo = scoreInfo.MaxCombo;
- double accuracyRatio = extractedMaxBaseScore > 0 ? extractedBaseScore / extractedMaxBaseScore : 1;
- double comboRatio = extractedMaxCombo > 0 ? (double)scoreInfo.MaxCombo / extractedMaxCombo : 1;
-
- return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), extractedBasicHitObjects);
+ return ComputeScore(mode, current, maximum);
}
///
@@ -284,17 +311,10 @@ namespace osu.Game.Rulesets.Scoring
if (!beatmapApplied)
throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}.");
- extractFromStatistics(ruleset,
- scoreInfo.Statistics,
- out double extractedBaseScore,
- out _,
- out _,
- out _);
+ extractScoringValues(scoreInfo.Statistics, out var current, out _);
+ current.MaxCombo = scoreInfo.MaxCombo;
- double accuracyRatio = MaxAchievableBaseScore > 0 ? extractedBaseScore / MaxAchievableBaseScore : 1;
- double comboRatio = MaxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / MaxAchievableCombo : 1;
-
- return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), TotalBasicHitObjects);
+ return ComputeScore(mode, current, MaximumScoringValues);
}
///
@@ -316,26 +336,29 @@ namespace osu.Game.Rulesets.Scoring
double accuracyRatio = scoreInfo.Accuracy;
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
+ extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
+
// For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score.
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
- if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3)
- {
- extractFromStatistics(
- ruleset,
- scoreInfo.Statistics,
- out double computedBaseScore,
- out double computedMaxBaseScore,
- out _,
- out _);
+ if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0)
+ accuracyRatio = current.BaseScore / maximum.BaseScore;
- if (computedMaxBaseScore > 0)
- accuracyRatio = computedBaseScore / computedMaxBaseScore;
- }
+ return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.HitObjects);
+ }
- int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum();
-
- return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), computedBasicHitObjects);
+ ///
+ /// Computes the total score from scoring values.
+ ///
+ /// The to represent the score as.
+ /// The current scoring values.
+ /// The maximum scoring values.
+ /// The total score computed from the given scoring values.
+ public double ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
+ {
+ double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1;
+ double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
+ return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.HitObjects);
}
///
@@ -365,15 +388,6 @@ namespace osu.Game.Rulesets.Scoring
}
}
- ///
- /// Calculates the total bonus score from score statistics.
- ///
- /// The score statistics.
- /// The total bonus score.
- private double getBonusScore(IReadOnlyDictionary statistics)
- => statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
- + statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
-
private ScoreRank rankFrom(double acc)
{
if (acc == 1)
@@ -405,15 +419,10 @@ namespace osu.Game.Rulesets.Scoring
lastHitObject = null;
if (storeResults)
- {
- MaxAchievableCombo = HighestCombo.Value;
- MaxAchievableBaseScore = rollingBaseScore;
- TotalBasicHitObjects = rollingBasicHitObjects;
- }
+ MaximumScoringValues = currentScoringValues;
- rollingBaseScore = 0;
- rollingMaxAchievableBaseScore = 0;
- rollingBasicHitObjects = 0;
+ currentScoringValues = default;
+ currentMaximumScoringValues = default;
TotalScore.Value = 0;
Accuracy.Value = 1;
@@ -440,14 +449,19 @@ namespace osu.Game.Rulesets.Scoring
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
}
- public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
+ public override void ResetFromReplayFrame(ReplayFrame frame)
{
- base.ResetFromReplayFrame(ruleset, frame);
+ base.ResetFromReplayFrame(frame);
if (frame.Header == null)
return;
- extractFromStatistics(ruleset, frame.Header.Statistics, out rollingBaseScore, out rollingMaxAchievableBaseScore, out _, out _);
+ extractScoringValues(frame.Header.Statistics, out var current, out var maximum);
+ currentScoringValues.BaseScore = current.BaseScore;
+ currentScoringValues.MaxCombo = frame.Header.MaxCombo;
+ currentMaximumScoringValues.BaseScore = maximum.BaseScore;
+ currentMaximumScoringValues.MaxCombo = maximum.MaxCombo;
+
HighestCombo.Value = frame.Header.MaxCombo;
scoreResultCounts.Clear();
@@ -458,52 +472,123 @@ namespace osu.Game.Rulesets.Scoring
OnResetFromReplayFrame?.Invoke();
}
- private void extractFromStatistics(Ruleset ruleset, IReadOnlyDictionary statistics, out double baseScore, out double maxBaseScore, out int maxCombo,
- out int basicHitObjects)
+ #region ScoringValue extraction
+
+ ///
+ /// Applies a best-effort extraction of hit statistics into .
+ ///
+ ///
+ /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
+ ///
+ /// - The maximum will always be 0.
+ /// - The current and maximum will always be the same value.
+ ///
+ /// Consumers are expected to more accurately fill in the above values through external means.
+ ///
+ /// Ensure to fill in the maximum for use in
+ /// .
+ ///
+ ///
+ /// The score to extract scoring values from.
+ /// The "current" scoring values, representing the hit statistics as they appear.
+ /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.
+ public void ExtractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
{
- baseScore = 0;
- maxBaseScore = 0;
- maxCombo = 0;
- basicHitObjects = 0;
+ extractScoringValues(scoreInfo.Statistics, out current, out maximum);
+ current.MaxCombo = scoreInfo.MaxCombo;
+ }
+
+ ///
+ /// Applies a best-effort extraction of hit statistics into .
+ ///
+ ///
+ /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
+ ///
+ /// - The maximum will always be 0.
+ /// - The current and maximum will always be the same value.
+ ///
+ /// Consumers are expected to more accurately fill in the above values through external means.
+ ///
+ /// Ensure to fill in the maximum for use in
+ /// .
+ ///
+ ///
+ /// The replay frame header to extract scoring values from.
+ /// The "current" scoring values, representing the hit statistics as they appear.
+ /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.
+ public void ExtractScoringValues(FrameHeader header, out ScoringValues current, out ScoringValues maximum)
+ {
+ extractScoringValues(header.Statistics, out current, out maximum);
+ current.MaxCombo = header.MaxCombo;
+ }
+
+ ///
+ /// Applies a best-effort extraction of hit statistics into .
+ ///
+ ///
+ /// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
+ ///
+ /// - The current will always be 0.
+ /// - The maximum will always be 0.
+ /// - The current and maximum will always be the same value.
+ ///
+ /// Consumers are expected to more accurately fill in the above values (especially the current ) via external means (e.g. ).
+ ///
+ /// The hit statistics to extract scoring values from.
+ /// The "current" scoring values, representing the hit statistics as they appear.
+ /// The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.
+ private void extractScoringValues(IReadOnlyDictionary statistics, out ScoringValues current, out ScoringValues maximum)
+ {
+ current = default;
+ maximum = default;
foreach ((HitResult result, int count) in statistics)
{
- // Bonus scores are counted separately directly from the statistics dictionary later on.
- if (!result.IsScorable() || result.IsBonus())
+ if (!result.IsScorable())
continue;
- // The maximum result of this judgement if it wasn't a miss.
- // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
- HitResult maxResult;
-
- switch (result)
+ if (result.IsBonus())
+ current.BonusScore += count * Judgement.ToNumericResult(result);
+ else
{
- case HitResult.LargeTickHit:
- case HitResult.LargeTickMiss:
- maxResult = HitResult.LargeTickHit;
- break;
+ // The maximum result of this judgement if it wasn't a miss.
+ // E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
+ HitResult maxResult;
- case HitResult.SmallTickHit:
- case HitResult.SmallTickMiss:
- maxResult = HitResult.SmallTickHit;
- break;
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ case HitResult.LargeTickMiss:
+ maxResult = HitResult.LargeTickHit;
+ break;
- default:
- maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
- break;
+ case HitResult.SmallTickHit:
+ case HitResult.SmallTickMiss:
+ maxResult = HitResult.SmallTickHit;
+ break;
+
+ default:
+ maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
+ break;
+ }
+
+ current.BaseScore += count * Judgement.ToNumericResult(result);
+ maximum.BaseScore += count * Judgement.ToNumericResult(maxResult);
}
- baseScore += count * Judgement.ToNumericResult(result);
- maxBaseScore += count * Judgement.ToNumericResult(maxResult);
-
if (result.AffectsCombo())
- maxCombo += count;
+ maximum.MaxCombo += count;
if (result.IsBasic())
- basicHitObjects += count;
+ {
+ current.HitObjects += count;
+ maximum.HitObjects += count;
+ }
}
}
+ #endregion
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index 7d1b23f48b..b5390eb6e2 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.UI
{
public readonly KeyBindingContainer KeyBindingContainer;
- private readonly Ruleset ruleset;
-
[Resolved(CanBeNull = true)]
private ScoreProcessor scoreProcessor { get; set; }
@@ -57,8 +55,6 @@ namespace osu.Game.Rulesets.UI
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
{
- this.ruleset = ruleset.CreateInstance();
-
InternalChild = KeyBindingContainer =
CreateKeyBindingContainer(ruleset, variant, unique)
.WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
@@ -85,7 +81,7 @@ namespace osu.Game.Rulesets.UI
break;
case ReplayStatisticsFrameEvent statisticsStateChangeEvent:
- scoreProcessor?.ResetFromReplayFrame(ruleset, statisticsStateChangeEvent.Frame);
+ scoreProcessor?.ResetFromReplayFrame(statisticsStateChangeEvent.Frame);
break;
default:
diff --git a/osu.Game/Scoring/ScoringValues.cs b/osu.Game/Scoring/ScoringValues.cs
new file mode 100644
index 0000000000..4b562c20e4
--- /dev/null
+++ b/osu.Game/Scoring/ScoringValues.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using MessagePack;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Scoring
+{
+ [MessagePackObject]
+ public struct ScoringValues
+ {
+ ///
+ /// The sum of all "basic" scoring values. See: and .
+ ///
+ [Key(0)]
+ public double BaseScore;
+
+ ///
+ /// The sum of all "bonus" scoring values. See: and .
+ ///
+ [Key(1)]
+ public double BonusScore;
+
+ ///
+ /// The highest achieved combo.
+ ///
+ [Key(2)]
+ public int MaxCombo;
+
+ ///
+ /// The count of "basic" s. See: .
+ ///
+ [Key(3)]
+ public int HitObjects;
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 79cfd7c917..eb47d0468f 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -29,13 +29,14 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index b1ba64beba..ccecad6f82 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -84,7 +84,7 @@
-
+