mirror of
https://github.com/ppy/osu.git
synced 2025-02-13 15:43:21 +08:00
Merge branch 'master' into health-less-value-changed
This commit is contained in:
commit
f912a1ba31
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@ -20,20 +21,73 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
private const int combo_cap = 200;
|
||||
private const double combo_base = 4;
|
||||
|
||||
private double fruitTinyScale;
|
||||
|
||||
public CatchScoreProcessor()
|
||||
: base(new CatchRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
||||
// large ticks are *purposefully* not counted to match stable
|
||||
int fruitTinyScaleDivisor = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) + MaximumResultCounts.GetValueOrDefault(HitResult.Great);
|
||||
fruitTinyScale = fruitTinyScaleDivisor == 0
|
||||
? 0
|
||||
: (double)MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor;
|
||||
}
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 600000 * comboProgress
|
||||
+ 400000 * Accuracy.Value * accuracyProgress
|
||||
const int max_tiny_droplets_portion = 400000;
|
||||
|
||||
double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale);
|
||||
double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale;
|
||||
double dropletsHit = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) == 0
|
||||
? 0
|
||||
: (double)ScoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
|
||||
return comboPortion * comboProgress
|
||||
+ dropletsPortion * dropletsHit
|
||||
+ bonusPortion;
|
||||
}
|
||||
|
||||
public override int GetBaseScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
// dirty hack to emulate accuracy on stable weighting every object equally in accuracy portion
|
||||
case HitResult.Great:
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.SmallTickHit:
|
||||
return 300;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return 200;
|
||||
}
|
||||
|
||||
return base.GetBaseScoreForResult(result);
|
||||
}
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
=> GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
{
|
||||
double baseIncrease = 0;
|
||||
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.Great:
|
||||
baseIncrease = 300;
|
||||
break;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
baseIncrease = 100;
|
||||
break;
|
||||
}
|
||||
|
||||
return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
}
|
||||
|
||||
public override ScoreRank RankFromAccuracy(double accuracy)
|
||||
{
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -66,6 +67,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
private Container<DrawableSliderRepeat> repeatContainer;
|
||||
private PausableSkinnableSound slidingSample;
|
||||
|
||||
private readonly LayoutValue drawSizeLayout;
|
||||
|
||||
public DrawableSlider()
|
||||
: this(null)
|
||||
{
|
||||
@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
};
|
||||
AddLayout(drawSizeLayout = new LayoutValue(Invalidation.DrawSize | Invalidation.MiscGeometry));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -246,21 +250,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Ball.UpdateProgress(completionProgress);
|
||||
SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);
|
||||
|
||||
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
||||
{
|
||||
if (hitObject is ITrackSnaking s)
|
||||
s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
|
||||
}
|
||||
foreach (DrawableSliderRepeat repeat in repeatContainer)
|
||||
repeat.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
|
||||
|
||||
Size = SliderBody?.Size ?? Vector2.Zero;
|
||||
OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero;
|
||||
|
||||
if (DrawSize != Vector2.Zero)
|
||||
if (!drawSizeLayout.IsValid)
|
||||
{
|
||||
var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize);
|
||||
Vector2 pos = Vector2.Divide(OriginPosition, DrawSize);
|
||||
foreach (var obj in NestedHitObjects)
|
||||
obj.RelativeAnchorPosition = childAnchorPosition;
|
||||
Ball.RelativeAnchorPosition = childAnchorPosition;
|
||||
obj.RelativeAnchorPosition = pos;
|
||||
Ball.RelativeAnchorPosition = pos;
|
||||
|
||||
drawSizeLayout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
|
||||
public partial class DrawableSliderRepeat : DrawableOsuHitObject
|
||||
{
|
||||
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
/// <summary>
|
||||
/// A component which tracks the current end snaking position of a slider.
|
||||
/// </summary>
|
||||
public interface ITrackSnaking
|
||||
{
|
||||
void UpdateSnakingPosition(Vector2 start, Vector2 end);
|
||||
}
|
||||
}
|
@ -938,6 +938,35 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("touch device mod still active", () => Game.SelectedMods.Value, () => Has.One.InstanceOf<ModTouchDevice>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExitSongSelectAndImmediatelyClickLogo()
|
||||
{
|
||||
Screens.Select.SongSelect songSelect = null;
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("press escape and then click logo immediately", () =>
|
||||
{
|
||||
InputManager.Key(Key.Escape);
|
||||
clickLogoWhenNotCurrent();
|
||||
});
|
||||
|
||||
void clickLogoWhenNotCurrent()
|
||||
{
|
||||
if (songSelect.IsCurrentScreen())
|
||||
Scheduler.AddOnce(clickLogoWhenNotCurrent);
|
||||
else
|
||||
{
|
||||
InputManager.MoveMouseTo(Game.ChildrenOfType<OsuLogo>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Func<Player> playToResults()
|
||||
{
|
||||
var player = playToCompletion();
|
||||
|
@ -2,8 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Logging;
|
||||
@ -98,15 +98,11 @@ namespace osu.Game.Database
|
||||
// can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
|
||||
realm.Write(r =>
|
||||
{
|
||||
// TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
|
||||
var files = r.All<RealmFile>().ToList();
|
||||
|
||||
foreach (var file in files)
|
||||
foreach (var file in r.All<RealmFile>().Filter(@$"{nameof(RealmFile.Usages)}.@count = 0"))
|
||||
{
|
||||
totalFiles++;
|
||||
|
||||
if (file.BacklinksCount > 0)
|
||||
continue;
|
||||
Debug.Assert(file.BacklinksCount == 0);
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -446,16 +446,42 @@ namespace osu.Game.Database
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// compare logic in `CatchScoreProcessor`.
|
||||
|
||||
// this could technically be slightly incorrect in the case of stable scores.
|
||||
// because large droplet misses are counted as full misses in stable scores,
|
||||
// `score.MaximumStatistics.GetValueOrDefault(Great)` will be equal to the count of fruits *and* large droplets
|
||||
// rather than just fruits (which was the intent).
|
||||
// this is not fixable without introducing an extra legacy score attribute dedicated for catch,
|
||||
// and this is a ballpark conversion process anyway, so attempt to trudge on.
|
||||
int fruitTinyScaleDivisor = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + score.MaximumStatistics.GetValueOrDefault(HitResult.Great);
|
||||
double fruitTinyScale = fruitTinyScaleDivisor == 0
|
||||
? 0
|
||||
: (double)score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor;
|
||||
|
||||
const int max_tiny_droplets_portion = 400000;
|
||||
|
||||
double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale);
|
||||
double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale;
|
||||
double dropletsHit = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) == 0
|
||||
? 0
|
||||
: (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
600000 * comboProportion
|
||||
+ 400000 * score.Accuracy
|
||||
comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss))
|
||||
+ dropletsPortion * dropletsHit
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// in the mania case accuracy actually changes between score V1 and score V2 / standardised
|
||||
// (PERFECT weighting changes from 300 to 305),
|
||||
// so for better accuracy recompute accuracy locally based on hit statistics and use that instead,
|
||||
double scoreV2Accuracy = ComputeAccuracy(score);
|
||||
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
850000 * comboProportion
|
||||
+ 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
|
||||
+ 150000 * Math.Pow(scoreV2Accuracy, 2 + 2 * scoreV2Accuracy)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
@ -470,6 +496,94 @@ namespace osu.Game.Database
|
||||
return convertedTotalScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// For catch, the general method of calculating the combo proportion used for other rulesets is generally useless.
|
||||
/// This is because in stable score V1, catch has quadratic score progression,
|
||||
/// while in stable score V2, score progression is logarithmic up to 200 combo and then linear.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This means that applying the naive rescale method to scores with lots of short combos (think 10x 100-long combos on a 1000-object map)
|
||||
/// by linearly rescaling the combo portion as given by score V1 leads to horribly underestimating it.
|
||||
/// Therefore this method attempts to counteract this by calculating the best case estimate for the combo proportion that takes all of the above into account.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The general idea is that aside from the <paramref name="scoreMaxCombo"/> which the player is known to have hit,
|
||||
/// the remaining misses are evenly distributed across the rest of the objects that give combo.
|
||||
/// This is therefore a worst-case estimate.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static double estimateComboProportionForCatch(int beatmapMaxCombo, int scoreMaxCombo, int scoreMissCount)
|
||||
{
|
||||
if (beatmapMaxCombo == 0)
|
||||
return 1;
|
||||
|
||||
if (scoreMaxCombo == 0)
|
||||
return 0;
|
||||
|
||||
if (beatmapMaxCombo == scoreMaxCombo)
|
||||
return 1;
|
||||
|
||||
double estimatedBestCaseTotal = estimateBestCaseComboTotal(beatmapMaxCombo);
|
||||
|
||||
int remainingCombo = beatmapMaxCombo - (scoreMaxCombo + scoreMissCount);
|
||||
double totalDroppedScore = 0;
|
||||
|
||||
int assumedLengthOfRemainingCombos = (int)Math.Floor((double)remainingCombo / scoreMissCount);
|
||||
|
||||
if (assumedLengthOfRemainingCombos > 0)
|
||||
{
|
||||
int assumedCombosCount = (int)Math.Floor((double)remainingCombo / assumedLengthOfRemainingCombos);
|
||||
totalDroppedScore += assumedCombosCount * estimateDroppedComboScoreAfterMiss(assumedLengthOfRemainingCombos);
|
||||
|
||||
remainingCombo -= assumedCombosCount * assumedLengthOfRemainingCombos;
|
||||
|
||||
if (remainingCombo > 0)
|
||||
totalDroppedScore += estimateDroppedComboScoreAfterMiss(remainingCombo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// there are so many misses that attempting to evenly divide remaining combo results in 0 length per combo,
|
||||
// i.e. all remaining judgements are combo breaks.
|
||||
// in that case, presume every single remaining object is a miss and did not give any combo score.
|
||||
totalDroppedScore = estimatedBestCaseTotal - estimateBestCaseComboTotal(scoreMaxCombo);
|
||||
}
|
||||
|
||||
return estimatedBestCaseTotal == 0
|
||||
? 1
|
||||
: 1 - Math.Clamp(totalDroppedScore / estimatedBestCaseTotal, 0, 1);
|
||||
|
||||
double estimateBestCaseComboTotal(int maxCombo)
|
||||
{
|
||||
if (maxCombo == 0)
|
||||
return 1;
|
||||
|
||||
double estimatedTotal = 0.5 * Math.Min(maxCombo, 2);
|
||||
|
||||
if (maxCombo <= 2)
|
||||
return estimatedTotal;
|
||||
|
||||
// int_2^x log_4(t) dt
|
||||
estimatedTotal += (Math.Min(maxCombo, 200) * (Math.Log(Math.Min(maxCombo, 200)) - 1) + 2 - Math.Log(4)) / Math.Log(4);
|
||||
|
||||
if (maxCombo <= 200)
|
||||
return estimatedTotal;
|
||||
|
||||
estimatedTotal += (maxCombo - 200) * Math.Log(200) / Math.Log(4);
|
||||
return estimatedTotal;
|
||||
}
|
||||
|
||||
double estimateDroppedComboScoreAfterMiss(int lengthOfComboAfterMiss)
|
||||
{
|
||||
if (lengthOfComboAfterMiss >= 200)
|
||||
lengthOfComboAfterMiss = 200;
|
||||
|
||||
// int_0^x (log_4(200) - log_4(t)) dt
|
||||
// note that this is an pessimistic estimate, i.e. it may subtract too much if the miss happened before reaching 200 combo
|
||||
return lengthOfComboAfterMiss * (1 + Math.Log(200) - Math.Log(lengthOfComboAfterMiss)) / Math.Log(4);
|
||||
}
|
||||
}
|
||||
|
||||
public static double ComputeAccuracy(ScoreInfo scoreInfo)
|
||||
{
|
||||
Ruleset ruleset = scoreInfo.Ruleset.CreateInstance();
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Game.IO;
|
||||
using Realms;
|
||||
|
||||
@ -11,5 +12,8 @@ namespace osu.Game.Models
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
|
||||
[Backlink(nameof(RealmNamedFileUsage.File))]
|
||||
public IQueryable<RealmNamedFileUsage> Usages { get; } = null!;
|
||||
}
|
||||
}
|
||||
|
@ -151,9 +151,12 @@ namespace osu.Game.Overlays
|
||||
base.Update();
|
||||
|
||||
if (!headerTextVisibilityCache.IsValid)
|
||||
{
|
||||
// These toolbox grouped may be contracted to only show icons.
|
||||
// For now, let's hide the header to avoid text truncation weirdness in such cases.
|
||||
headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint);
|
||||
headerTextVisibilityCache.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
|
||||
|
@ -599,7 +599,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
float balanceAdjustAmount = positionalHitsoundsLevel.Value * 2;
|
||||
double returnedValue = balanceAdjustAmount * (position - 0.5f);
|
||||
|
||||
return returnedValue;
|
||||
// Rounded to reduce the overhead of audio adjustments (which are currently bindable heavy).
|
||||
// Balance is very hard to perceive in small increments anyways.
|
||||
return Math.Round(returnedValue, 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -169,14 +169,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (!beatmapApplied)
|
||||
throw new InvalidOperationException($"Cannot access maximum statistics before calling {nameof(ApplyBeatmap)}.");
|
||||
|
||||
return new Dictionary<HitResult, int>(maximumResultCounts);
|
||||
return new Dictionary<HitResult, int>(MaximumResultCounts);
|
||||
}
|
||||
}
|
||||
|
||||
private bool beatmapApplied;
|
||||
|
||||
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
|
||||
private readonly Dictionary<HitResult, int> maximumResultCounts = new Dictionary<HitResult, int>();
|
||||
protected readonly Dictionary<HitResult, int> ScoreResultCounts = new Dictionary<HitResult, int>();
|
||||
protected readonly Dictionary<HitResult, int> MaximumResultCounts = new Dictionary<HitResult, int>();
|
||||
|
||||
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
|
||||
private HitObject? lastHitObject;
|
||||
@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (result.FailedAtJudgement)
|
||||
return;
|
||||
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
|
||||
ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) + 1;
|
||||
|
||||
if (result.Type.IncreasesCombo())
|
||||
Combo.Value++;
|
||||
@ -278,7 +278,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (result.FailedAtJudgement)
|
||||
return;
|
||||
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
|
||||
ScoreResultCounts[result.Type] = ScoreResultCounts.GetValueOrDefault(result.Type) - 1;
|
||||
|
||||
if (result.Judgement.MaxResult.AffectsAccuracy())
|
||||
{
|
||||
@ -400,13 +400,13 @@ namespace osu.Game.Rulesets.Scoring
|
||||
maximumComboPortion = currentComboPortion;
|
||||
maximumAccuracyJudgementCount = currentAccuracyJudgementCount;
|
||||
|
||||
maximumResultCounts.Clear();
|
||||
maximumResultCounts.AddRange(scoreResultCounts);
|
||||
MaximumResultCounts.Clear();
|
||||
MaximumResultCounts.AddRange(ScoreResultCounts);
|
||||
|
||||
MaximumTotalScore = TotalScore.Value;
|
||||
}
|
||||
|
||||
scoreResultCounts.Clear();
|
||||
ScoreResultCounts.Clear();
|
||||
|
||||
currentBaseScore = 0;
|
||||
currentMaximumBaseScore = 0;
|
||||
@ -435,10 +435,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
score.MaximumStatistics.Clear();
|
||||
|
||||
foreach (var result in HitResultExtensions.ALL_TYPES)
|
||||
score.Statistics[result] = scoreResultCounts.GetValueOrDefault(result);
|
||||
score.Statistics[result] = ScoreResultCounts.GetValueOrDefault(result);
|
||||
|
||||
foreach (var result in HitResultExtensions.ALL_TYPES)
|
||||
score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result);
|
||||
score.MaximumStatistics[result] = MaximumResultCounts.GetValueOrDefault(result);
|
||||
|
||||
// Populate total score after everything else.
|
||||
score.TotalScore = TotalScore.Value;
|
||||
@ -469,8 +469,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
HighestCombo.Value = frame.Header.MaxCombo;
|
||||
TotalScore.Value = frame.Header.TotalScore;
|
||||
|
||||
scoreResultCounts.Clear();
|
||||
scoreResultCounts.AddRange(frame.Header.Statistics);
|
||||
ScoreResultCounts.Clear();
|
||||
ScoreResultCounts.AddRange(frame.Header.Statistics);
|
||||
|
||||
SetScoreProcessorStatistics(frame.Header.ScoreProcessorStatistics);
|
||||
|
||||
|
@ -36,9 +36,11 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// <item><description>30000007: Adjust osu!mania combo and accuracy portions and judgement scoring values. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000008: Add accuracy conversion. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000010: Fix mania score V1 conversion using score V1 accuracy rather than V2 accuracy. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000011: Re-do catch scoring to mirror stable Score V2 as closely as feasible. Reconvert all scores.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000009;
|
||||
public const int LATEST_VERSION = 30000011;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
@ -83,12 +83,14 @@ namespace osu.Game.Screens.Play.HUD
|
||||
},
|
||||
fractionPart = new ArgonCounterTextComponent(Anchor.TopLeft)
|
||||
{
|
||||
RequiredDisplayDigits = { Value = 2 },
|
||||
WireframeOpacity = { BindTarget = WireframeOpacity },
|
||||
Scale = new Vector2(0.5f),
|
||||
},
|
||||
percentText = new ArgonCounterTextComponent(Anchor.TopLeft)
|
||||
{
|
||||
Text = @"%",
|
||||
RequiredDisplayDigits = { Value = 1 },
|
||||
WireframeOpacity = { BindTarget = WireframeOpacity }
|
||||
},
|
||||
}
|
||||
|
@ -57,6 +57,31 @@ namespace osu.Game.Screens.Play.HUD
|
||||
});
|
||||
}
|
||||
|
||||
public override int DisplayedCount
|
||||
{
|
||||
get => base.DisplayedCount;
|
||||
set
|
||||
{
|
||||
base.DisplayedCount = value;
|
||||
updateWireframe();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWireframe()
|
||||
{
|
||||
text.RequiredDisplayDigits.Value = getDigitsRequiredForDisplayCount();
|
||||
}
|
||||
|
||||
private int getDigitsRequiredForDisplayCount()
|
||||
{
|
||||
// one for the single presumed starting digit, one for the "x" at the end.
|
||||
int digitsRequired = 2;
|
||||
long c = DisplayedCount;
|
||||
while ((c /= 10) > 0)
|
||||
digitsRequired++;
|
||||
return digitsRequired;
|
||||
}
|
||||
|
||||
protected override LocalisableString FormatCount(int count) => $@"{count}x";
|
||||
|
||||
protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, MatchesStrings.MatchScoreStatsCombo.ToUpper())
|
||||
|
@ -2,7 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -33,14 +33,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => textPart.Text;
|
||||
set
|
||||
{
|
||||
int remainingCount = RequiredDisplayDigits.Value - value.ToString().Count(char.IsDigit);
|
||||
string remainingText = remainingCount > 0 ? new string('#', remainingCount) : string.Empty;
|
||||
|
||||
wireframesPart.Text = remainingText + value;
|
||||
textPart.Text = value;
|
||||
}
|
||||
set => textPart.Text = value;
|
||||
}
|
||||
|
||||
public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null)
|
||||
@ -81,6 +74,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RequiredDisplayDigits.BindValueChanged(digits => wireframesPart.Text = new string('#', digits.NewValue));
|
||||
}
|
||||
|
||||
private string textLookup(char c)
|
||||
@ -137,33 +132,49 @@ namespace osu.Game.Screens.Play.HUD
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
{
|
||||
const string font_name = @"argon-counter";
|
||||
|
||||
Spacing = new Vector2(-2f, 0f);
|
||||
Font = new FontUsage(@"argon-counter", 1);
|
||||
glyphStore = new GlyphStore(textures, getLookup);
|
||||
Font = new FontUsage(font_name, 1);
|
||||
glyphStore = new GlyphStore(font_name, textures, getLookup);
|
||||
}
|
||||
|
||||
protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore);
|
||||
|
||||
private class GlyphStore : ITexturedGlyphLookupStore
|
||||
{
|
||||
private readonly string fontName;
|
||||
private readonly TextureStore textures;
|
||||
private readonly Func<char, string> getLookup;
|
||||
|
||||
public GlyphStore(TextureStore textures, Func<char, string> getLookup)
|
||||
private readonly Dictionary<char, ITexturedCharacterGlyph?> cache = new Dictionary<char, ITexturedCharacterGlyph?>();
|
||||
|
||||
public GlyphStore(string fontName, TextureStore textures, Func<char, string> getLookup)
|
||||
{
|
||||
this.fontName = fontName;
|
||||
this.textures = textures;
|
||||
this.getLookup = getLookup;
|
||||
}
|
||||
|
||||
public ITexturedCharacterGlyph? Get(string? fontName, char character)
|
||||
{
|
||||
// We only service one font.
|
||||
if (fontName != this.fontName)
|
||||
return null;
|
||||
|
||||
if (cache.TryGetValue(character, out var cached))
|
||||
return cached;
|
||||
|
||||
string lookup = getLookup(character);
|
||||
var texture = textures.Get($"Gameplay/Fonts/{fontName}-{lookup}");
|
||||
|
||||
if (texture == null)
|
||||
return null;
|
||||
TexturedCharacterGlyph? glyph = null;
|
||||
|
||||
return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 0.125f);
|
||||
if (texture != null)
|
||||
glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 0.125f);
|
||||
|
||||
cache[character] = glyph;
|
||||
return glyph;
|
||||
}
|
||||
|
||||
public Task<ITexturedCharacterGlyph?> GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character));
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -72,6 +73,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private readonly LayoutValue drawSizeLayout = new LayoutValue(Invalidation.DrawSize);
|
||||
|
||||
private readonly Cached pathVerticesCache = new Cached();
|
||||
|
||||
public ArgonHealthDisplay()
|
||||
{
|
||||
AddLayout(drawSizeLayout);
|
||||
@ -311,6 +314,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
glowBar.Vertices = vertices;
|
||||
glowBar.Position = initialVertex;
|
||||
|
||||
pathVerticesCache.Validate();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -15,6 +16,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class ArgonScoreCounter : GameplayScoreCounter, ISerialisableDrawable
|
||||
{
|
||||
private ArgonScoreTextComponent scoreText = null!;
|
||||
|
||||
protected override double RollingDuration => 500;
|
||||
protected override Easing RollingEasing => Easing.OutQuint;
|
||||
|
||||
@ -33,13 +36,42 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
protected override LocalisableString FormatCount(long count) => count.ToLocalisableString();
|
||||
|
||||
protected override IHasText CreateText() => new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper())
|
||||
protected override IHasText CreateText() => scoreText = new ArgonScoreTextComponent(Anchor.TopRight, BeatmapsetsStrings.ShowScoreboardHeadersScore.ToUpper())
|
||||
{
|
||||
RequiredDisplayDigits = { BindTarget = RequiredDisplayDigits },
|
||||
WireframeOpacity = { BindTarget = WireframeOpacity },
|
||||
ShowLabel = { BindTarget = ShowLabel },
|
||||
};
|
||||
|
||||
public ArgonScoreCounter()
|
||||
{
|
||||
RequiredDisplayDigits.BindValueChanged(_ => updateWireframe());
|
||||
}
|
||||
|
||||
public override long DisplayedCount
|
||||
{
|
||||
get => base.DisplayedCount;
|
||||
set
|
||||
{
|
||||
base.DisplayedCount = value;
|
||||
updateWireframe();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWireframe()
|
||||
{
|
||||
scoreText.RequiredDisplayDigits.Value =
|
||||
Math.Max(RequiredDisplayDigits.Value, getDigitsRequiredForDisplayCount());
|
||||
}
|
||||
|
||||
private int getDigitsRequiredForDisplayCount()
|
||||
{
|
||||
int digitsRequired = 1;
|
||||
long c = DisplayedCount;
|
||||
while ((c /= 10) > 0)
|
||||
digitsRequired++;
|
||||
return digitsRequired;
|
||||
}
|
||||
|
||||
private partial class ArgonScoreTextComponent : ArgonCounterTextComponent
|
||||
{
|
||||
public ArgonScoreTextComponent(Anchor anchor, LocalisableString? label = null)
|
||||
|
@ -11,11 +11,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
protected ComboCounter()
|
||||
{
|
||||
Current.Value = DisplayedCount = 0;
|
||||
}
|
||||
|
||||
protected override double GetProportionalDuration(int currentValue, int newValue)
|
||||
{
|
||||
return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f;
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
public Bindable<double> Current { get; } = new BindableDouble
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 1
|
||||
MaxValue = 1,
|
||||
};
|
||||
|
||||
private BindableNumber<double> health = null!;
|
||||
|
@ -660,7 +660,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
logo.Action = () =>
|
||||
{
|
||||
FinaliseSelection();
|
||||
if (this.IsCurrentScreen())
|
||||
FinaliseSelection();
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -44,10 +45,11 @@ namespace osu.Game.Skinning
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
base.Font = new FontUsage(skin.GetFontPrefix(font), 1, fixedWidth: FixedWidth);
|
||||
string fontPrefix = skin.GetFontPrefix(font);
|
||||
base.Font = new FontUsage(fontPrefix, 1, fixedWidth: FixedWidth);
|
||||
Spacing = new Vector2(-skin.GetFontOverlap(font), 0);
|
||||
|
||||
glyphStore = new LegacyGlyphStore(skin, MaxSizePerGlyph);
|
||||
glyphStore = new LegacyGlyphStore(fontPrefix, skin, MaxSizePerGlyph);
|
||||
}
|
||||
|
||||
protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore);
|
||||
@ -57,25 +59,42 @@ namespace osu.Game.Skinning
|
||||
private readonly ISkin skin;
|
||||
private readonly Vector2? maxSize;
|
||||
|
||||
public LegacyGlyphStore(ISkin skin, Vector2? maxSize)
|
||||
private readonly string fontName;
|
||||
|
||||
private readonly Dictionary<char, ITexturedCharacterGlyph?> cache = new Dictionary<char, ITexturedCharacterGlyph?>();
|
||||
|
||||
public LegacyGlyphStore(string fontName, ISkin skin, Vector2? maxSize)
|
||||
{
|
||||
this.fontName = fontName;
|
||||
this.skin = skin;
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public ITexturedCharacterGlyph? Get(string? fontName, char character)
|
||||
{
|
||||
// We only service one font.
|
||||
if (fontName != this.fontName)
|
||||
return null;
|
||||
|
||||
if (cache.TryGetValue(character, out var cached))
|
||||
return cached;
|
||||
|
||||
string lookup = getLookupName(character);
|
||||
|
||||
var texture = skin.GetTexture($"{fontName}-{lookup}");
|
||||
|
||||
if (texture == null)
|
||||
return null;
|
||||
TexturedCharacterGlyph? glyph = null;
|
||||
|
||||
if (maxSize != null)
|
||||
texture = texture.WithMaximumSize(maxSize.Value);
|
||||
if (texture != null)
|
||||
{
|
||||
if (maxSize != null)
|
||||
texture = texture.WithMaximumSize(maxSize.Value);
|
||||
|
||||
return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 1f / texture.ScaleAdjust);
|
||||
glyph = new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 1f / texture.ScaleAdjust);
|
||||
}
|
||||
|
||||
cache[character] = glyph;
|
||||
return glyph;
|
||||
}
|
||||
|
||||
private static string getLookupName(char character)
|
||||
|
@ -194,9 +194,33 @@ namespace osu.Game.Skinning
|
||||
/// <summary>
|
||||
/// Whether any samples are currently playing.
|
||||
/// </summary>
|
||||
public bool IsPlaying => samplesContainer.Any(s => s.Playing);
|
||||
public bool IsPlaying
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (PoolableSkinnableSample s in samplesContainer)
|
||||
{
|
||||
if (s.Playing)
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool IsPlayed => samplesContainer.Any(s => s.Played);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsPlayed
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (PoolableSkinnableSample s in samplesContainer)
|
||||
{
|
||||
if (s.Played)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public IBindable<double> AggregateVolume => samplesContainer.AggregateVolume;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user