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

Merge branch 'master' into health-less-value-changed

This commit is contained in:
Dean Herbert 2024-01-10 18:03:34 +09:00
commit f912a1ba31
No known key found for this signature in database
22 changed files with 392 additions and 86 deletions

View File

@ -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)
{

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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
{

View File

@ -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();

View File

@ -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!;
}
}

View File

@ -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)

View File

@ -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>

View File

@ -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);

View File

@ -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.

View File

@ -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 }
},
}

View File

@ -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())

View File

@ -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));

View File

@ -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)

View File

@ -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)

View File

@ -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;

View File

@ -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!;

View File

@ -660,7 +660,8 @@ namespace osu.Game.Screens.Select
logo.Action = () =>
{
FinaliseSelection();
if (this.IsCurrentScreen())
FinaliseSelection();
return false;
};
}

View File

@ -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)

View File

@ -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;