[逐步优化]修改性能分析控件加载逻辑

This commit is contained in:
LA
2026-01-24 19:02:39 +08:00
parent 98188ecef8
commit be36aab648
11 changed files with 521 additions and 1118 deletions

View File

@@ -270,19 +270,13 @@ namespace osu.Game.LAsEzExtensions.Audio
{
activeCandidateTrack?.Seek(sliceStart);
try
{
if (candidateMuteAdjustment != null) activeCandidateTrack.RemoveAdjustment(AdjustableProperty.Volume, candidateMuteAdjustment);
}
catch { }
if (candidateMuteAdjustment != null)
activeCandidateTrack?.RemoveAdjustment(AdjustableProperty.Volume, candidateMuteAdjustment);
activeCandidateTrack?.Start();
Logger.Log("DuplicateVirtualTrack: restarted candidate after interval", LoggingTarget.Runtime);
inLoopDelay = false;
try { ensureLoopCheckerRunning(); }
catch { }
ensureLoopCheckerRunning();
}
catch (Exception ex)
{
@@ -485,19 +479,11 @@ namespace osu.Game.LAsEzExtensions.Audio
{
activeCandidateTrack?.Seek(sliceStart);
try
{
if (candidateMuteAdjustment != null) activeCandidateTrack.RemoveAdjustment(AdjustableProperty.Volume, candidateMuteAdjustment);
}
catch { }
if (candidateMuteAdjustment != null) activeCandidateTrack?.RemoveAdjustment(AdjustableProperty.Volume, candidateMuteAdjustment);
activeCandidateTrack?.Start();
Log("restarted candidate after interval");
inLoopDelay = false;
try { ensureLoopCheckerRunning(); }
catch { }
ensureLoopCheckerRunning();
}
catch (Exception ex)
{

View File

@@ -48,7 +48,6 @@ namespace osu.Game.LAsEzExtensions.Audio
private const double audio_resync_tolerance = 50; // ms
private const double audio_resync_cooldown = 120; // ms
private double lastAudioResyncClockTime;
private const double max_dynamic_preview_length = 60000; // 动态扩展最长 ms
private readonly SampleSchedulerState sampleScheduler = new SampleSchedulerState();
private readonly PlaybackState playback = new PlaybackState();

View File

@@ -116,7 +116,7 @@ namespace osu.Game.LAsEzExtensions.Configuration
private void initializeManiaDefaults()
{
SetDefault(Ez2Setting.KpcDisplayMode, EzKpcDisplay.KpcDisplayMode.Numbers);
SetDefault(Ez2Setting.KpcDisplayMode, KpcDisplayMode.Numbers);
SetDefault(Ez2Setting.XxySRFilter, false);
SetDefault(Ez2Setting.KeySoundPreview, false);
SetDefault(Ez2Setting.EzSelectCsMode, "");

View File

@@ -9,39 +9,34 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Framework.Layout;
using osu.Game.LAsEzExtensions.Analysis;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.LAsEzExtensions.UserInterface
{
public partial class EzDisplayLineGraph : Container
public partial class EzDisplayKpsGraph : CompositeDrawable
{
public float? MaxValue { get; set; }
private const int max_draw_points = OptimizedBeatmapCalculator.DEFAULT_KPS_GRAPH_POINTS;
public float? MinValue { get; set; }
private readonly LayoutValue pathCached = new LayoutValue(Invalidation.DrawSize);
private readonly Path path;
// private readonly BufferedContainer bufferedHost;
private float[] values;
public float ActualMaxValue { get; private set; } = float.NaN;
public float ActualMinValue { get; private set; } = float.NaN;
private const double transform_duration = 1500;
public int DefaultValueCount;
public float? MaxValue { get; set; }
public float? MinValue { get; set; }
private const double transform_duration = 1000;
private readonly Container<Path> maskingContainer;
private readonly Path path;
private float[] values;
private int valuesCount;
public Color4 LineColour
public EzDisplayKpsGraph()
{
get => maskingContainer.Colour;
set => maskingContainer.Colour = value;
}
public EzDisplayLineGraph()
{
Add(maskingContainer = new Container<Path>
AddInternal(maskingContainer = new Container<Path>
{
Masking = true,
RelativeSizeAxes = Axes.Both,
@@ -49,14 +44,15 @@ namespace osu.Game.LAsEzExtensions.UserInterface
{
AutoSizeAxes = Axes.None,
RelativeSizeAxes = Axes.Both,
PathRadius = 1
PathRadius = 1.5f,
Colour = Colour4.CornflowerBlue
}
});
AddLayout(pathCached);
}
public void SetValues(IReadOnlyList<double> source)
public void SetPoints(IReadOnlyList<double> source)
{
if (source == null)
return;
@@ -98,8 +94,6 @@ namespace osu.Game.LAsEzExtensions.UserInterface
maskingContainer.ResizeWidthTo(1, transform_duration, Easing.OutQuint);
}
private readonly LayoutValue pathCached = new LayoutValue(Invalidation.DrawSize);
protected override void Update()
{
base.Update();
@@ -119,12 +113,23 @@ namespace osu.Game.LAsEzExtensions.UserInterface
if (count <= 0)
return;
int totalCount = Math.Max(count, DefaultValueCount);
int totalCount = Math.Max(count, max_draw_points);
// Use the path's own draw size so vertices are in the path's local space.
float availableWidth = Math.Max(0, path.DrawWidth - 2 * path.PathRadius);
float availableHeight = Math.Max(0, path.DrawHeight - 2 * path.PathRadius);
// Fallback to parent draw size if path hasn't been sized yet.
if (availableWidth <= 0) availableWidth = Math.Max(0, DrawWidth - 2 * path.PathRadius);
if (availableHeight <= 0) availableHeight = Math.Max(0, DrawHeight - 2 * path.PathRadius);
int denom = Math.Max(1, totalCount - 1);
for (int i = 0; i < count; i++)
{
float x = (i + totalCount - count) / (float)(totalCount - 1) * (DrawWidth - 2 * path.PathRadius);
float y = GetYPosition(values[i]) * (DrawHeight - 2 * path.PathRadius);
// account for the radius margin so vertices are inset by PathRadius on all sides
float x = path.PathRadius + (i + totalCount - count) / (float)denom * availableWidth;
float y = path.PathRadius + GetYPosition(values[i]) * availableHeight;
path.AddVertex(new Vector2(x, y));
}
}

View File

@@ -528,11 +528,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
List<RealmKeyBinding> bindings = GetAllSectionBindings();
// 检查是否是Mania规则集的操作
var actionType = Action.GetType();
bool isManiaAction = actionType.Namespace?.StartsWith("osu.Game.Rulesets.Mania", StringComparison.Ordinal) == true;
RealmKeyBinding? existingBinding = isManiaAction || keyBinding.KeyCombination.Equals(new KeyCombination(InputKey.None))
RealmKeyBinding? existingBinding = keyBinding.KeyCombination.Equals(new KeyCombination(InputKey.None))
? null
: bindings.FirstOrDefault(other => isConflictingBinding(keyBinding, other, restoringDefaults));

View File

@@ -17,7 +17,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Collections;
@@ -26,17 +25,17 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.LAsEzExtensions.Analysis;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.LAsEzExtensions.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.SelectV2;
using osuTK;
using osuTK.Graphics;
using osu.Game.LAsEzExtensions.Analysis;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.LAsEzExtensions.UserInterface;
using osu.Game.Screens.SelectV2;
using CommonStrings = osu.Game.Localisation.CommonStrings;
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
@@ -70,12 +69,23 @@ namespace osu.Game.Screens.Select.Carousel
private OsuSpriteText keyCountText = null!;
private EzDisplayLineGraph ezKpsGraph = null!;
private EzDisplayKpsGraph ezDisplayKpsGraph = null!;
private EzKpsDisplay ezKpsDisplay = null!;
private EzKpcDisplay ezKpcDisplay = null!;
// private EzKpcDisplay ezKpcDisplay = null!;
private EzDisplayXxySR displayXxySR = null!;
private Bindable<bool> xxySrFilterSetting = null!;
private int keyCount;
private IBindable<ManiaBeatmapAnalysisResult>? maniaAnalysisBindable;
private CancellationTokenSource? maniaAnalysisCancellationSource;
private string? cachedScratchText;
private Dictionary<int, int>? normalizedColumnCounts;
private Dictionary<int, int>? normalizedHoldNoteCounts;
private int normalizedCountsKeyCount;
[Resolved]
private Ez2ConfigManager ezConfig { get; set; } = null!;
@@ -112,30 +122,6 @@ namespace osu.Game.Screens.Select.Carousel
private IBindable<StarDifficulty> starDifficultyBindable = null!;
private CancellationTokenSource? starDifficultyCancellationSource;
private IBindable<ManiaBeatmapAnalysisResult>? maniaAnalysisBindable;
private CancellationTokenSource? maniaAnalysisCancellationSource;
private string? cachedScratchText;
private ScheduledDelegate? scheduledManiaUiUpdate;
private (double averageKps, double maxKps, List<double> kpsList) pendingKpsResult;
private Dictionary<int, int>? pendingColumnCounts;
private Dictionary<int, int>? pendingHoldNoteCounts;
private bool hasPendingUiUpdate;
private Bindable<EzKpcDisplay.KpcDisplayMode> kpcDisplayMode = null!;
private int cachedKpcKeyCount = -1;
private Guid cachedKpcBeatmapId;
private int cachedKpcRulesetId = -1;
private int cachedKpcModsHash;
private Dictionary<int, int>? normalizedColumnCounts;
private Dictionary<int, int>? normalizedHoldNoteCounts;
private int normalizedCountsKeyCount;
private int lastKpcCountsHash;
private EzKpcDisplay.KpcDisplayMode lastKpcMode;
public DrawableCarouselBeatmap(CarouselBeatmap panel)
{
beatmapInfo = panel.BeatmapInfo;
@@ -199,7 +185,7 @@ namespace osu.Game.Screens.Select.Carousel
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
ezKpsGraph = new EzDisplayLineGraph
ezDisplayKpsGraph = new EzDisplayKpsGraph
{
Size = new Vector2(300, 20),
Anchor = Anchor.BottomLeft,
@@ -257,11 +243,11 @@ namespace osu.Game.Screens.Select.Carousel
Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.875f),
},
ezKpcDisplay = new EzKpcDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
// ezKpcDisplay = new EzKpcDisplay
// {
// Anchor = Anchor.CentreLeft,
// Origin = Anchor.CentreLeft,
// }
}
}
}
@@ -277,7 +263,7 @@ namespace osu.Game.Screens.Select.Carousel
ruleset.BindValueChanged(_ =>
{
computeManiaAnalysis();
resetManiaAnalysisDisplay();
updateKeyCount();
});
@@ -295,12 +281,6 @@ namespace osu.Game.Screens.Select.Carousel
starCounter.Icon = value.NewValue
? FontAwesome.Solid.Moon
: FontAwesome.Solid.Star;
}, true); // true 表示立即触发一次以设置初始状态
kpcDisplayMode = ezConfig.GetBindable<EzKpcDisplay.KpcDisplayMode>(Ez2Setting.KpcDisplayMode);
kpcDisplayMode.BindValueChanged(mode =>
{
ezKpcDisplay.CurrentKpcDisplayMode = mode.NewValue;
}, true);
}
@@ -341,6 +321,7 @@ namespace osu.Game.Screens.Select.Carousel
starCounter.ReplayAnimation();
starDifficultyCancellationSource?.Cancel();
maniaAnalysisCancellationSource?.Cancel();
// Only compute difficulty when the item is visible.
if (Item?.State.Value != CarouselItemState.Collapsed)
@@ -362,64 +343,20 @@ namespace osu.Game.Screens.Select.Carousel
base.ApplyState();
}
private void queueManiaUiUpdate((double averageKps, double maxKps, List<double> kpsList) result, Dictionary<int, int>? columnCounts, Dictionary<int, int>? holdNoteCounts)
{
pendingKpsResult = result;
pendingColumnCounts = columnCounts;
pendingHoldNoteCounts = holdNoteCounts;
hasPendingUiUpdate = true;
if (scheduledManiaUiUpdate != null)
return;
scheduledManiaUiUpdate = Scheduler.AddDelayed(() =>
{
scheduledManiaUiUpdate = null;
if (!hasPendingUiUpdate)
return;
hasPendingUiUpdate = false;
updateKPs(pendingKpsResult, pendingColumnCounts, pendingHoldNoteCounts);
}, mania_ui_update_throttle_ms, false);
}
private void resetManiaAnalysisDisplay()
{
cachedScratchText = null;
displayXxySR.Current.Value = null;
if (ruleset.Value.OnlineID == 3)
{
ezKpcDisplay.Show();
// ezKpcDisplay.Show();
displayXxySR.Show();
}
else
{
ezKpcDisplay.Hide();
// ezKpcDisplay.Hide();
displayXxySR.Hide();
}
}
private int getCachedKpcKeyCount()
{
Guid beatmapId = beatmapInfo.ID;
int rulesetId = ruleset.Value.OnlineID;
int modsHash = computeModsHash(mods.Value);
if (cachedKpcKeyCount >= 0
&& cachedKpcBeatmapId == beatmapId
&& cachedKpcRulesetId == rulesetId
&& cachedKpcModsHash == modsHash)
return cachedKpcKeyCount;
// legacy KPC key count calculation intentionally left unimplemented here.
cachedKpcBeatmapId = beatmapId;
cachedKpcRulesetId = rulesetId;
cachedKpcModsHash = modsHash;
return cachedKpcKeyCount;
}
private void ensureNormalizedCounts(int keyCount)
{
// Defensive: ensure keyCount is non-negative. Some legacy paths may
@@ -442,85 +379,27 @@ namespace osu.Game.Screens.Select.Carousel
}
}
private static int computeModsHash(IReadOnlyList<Mod> mods)
{
unchecked
{
int hash = 17;
for (int i = 0; i < mods.Count; i++)
hash = hash * 31 + mods[i].GetHashCode();
return hash;
}
}
private static int computeCountsHash(Dictionary<int, int> columnCounts, Dictionary<int, int> holdCounts, int keyCount)
{
unchecked
{
int hash = 17;
for (int i = 0; i < keyCount; i++)
{
hash = hash * 31 + columnCounts.GetValueOrDefault(i);
hash = hash * 31 + holdCounts.GetValueOrDefault(i);
}
return hash;
}
}
private void updateKPs((double averageKps, double maxKps, List<double> kpsList) result, Dictionary<int, int>? columnCounts, Dictionary<int, int>? holdNoteCounts)
{
if (Item == null)
return;
// 滚动过程中会有大量不可见/刚离屏的面板仍收到分析回调。
// 这些面板的 UI 更新会造成明显 GC 压力与 Draw FPS 下降,因此先缓存为 pending等再次可见时再应用。
if (!IsPresent)
{
pendingKpsResult = result;
pendingColumnCounts = columnCounts;
pendingHoldNoteCounts = holdNoteCounts;
hasPendingUiUpdate = true;
if (Item?.State.Value == CarouselItemState.Collapsed)
return;
}
var (averageKps, maxKps, kpsList) = result;
ezKpsDisplay.SetKps(averageKps, maxKps);
// Update KPS graph with the KPS list
if (kpsList.Count > 0)
{
ezKpsGraph.SetValues(kpsList);
ezDisplayKpsGraph.SetPoints(kpsList);
}
if (columnCounts != null)
{
// 注意:分析结果里的 ColumnCounts 只包含“出现过的列”。
// 当某个 mod 删除了某一列的所有 notes 时,这一列会缺失,
// 直接显示会导致列号错位(看起来像“没有更新”)。
// 这里把字典补齐到 0..keyCount-1缺失列填 0。
int keyCount = getCachedKpcKeyCount();
ensureNormalizedCounts(keyCount);
for (int i = 0; i < keyCount; i++)
{
normalizedColumnCounts![i] = columnCounts.GetValueOrDefault(i);
normalizedHoldNoteCounts![i] = holdNoteCounts?.GetValueOrDefault(i) ?? 0;
}
int countsHash = computeCountsHash(normalizedColumnCounts!, normalizedHoldNoteCounts!, keyCount);
var mode = ezKpcDisplay.CurrentKpcDisplayMode;
if (countsHash != lastKpcCountsHash || mode != lastKpcMode)
{
lastKpcCountsHash = countsHash;
lastKpcMode = mode;
ezKpcDisplay.UpdateColumnCounts(normalizedColumnCounts!, normalizedHoldNoteCounts!);
}
}
// if (columnCounts != null)
// {
// ezKpcDisplay.UpdateColumnCounts(columnCounts, holdNoteCounts, keyCount);
// }
}
private void computeManiaAnalysis()
@@ -531,22 +410,16 @@ namespace osu.Game.Screens.Select.Carousel
if (Item == null)
return;
// Reset UI to avoid showing stale data from previous beatmap
resetManiaAnalysisDisplay();
if (Item?.State.Value == CarouselItemState.Collapsed)
return;
maniaAnalysisBindable = maniaAnalysisCache.GetBindableAnalysis(beatmapInfo, maniaAnalysisCancellationSource.Token, computationDelay: 100);
maniaAnalysisBindable.BindValueChanged(result =>
{
// Ignore placeholder handling use whatever real data is provided.
// Always update cachedScratchText (may be empty) so 0-note columns are reflected.
cachedScratchText = result.NewValue.ScratchText;
Schedule(updateKeyCount);
queueManiaUiUpdate((result.NewValue.AverageKps, result.NewValue.MaxKps, result.NewValue.KpsList), result.NewValue.ColumnCounts, result.NewValue.HoldNoteCounts);
if (result.NewValue.XxySr != null)
displayXxySR.Current.Value = result.NewValue.XxySr;
updateKPs((result.NewValue.AverageKps, result.NewValue.MaxKps, result.NewValue.KpsList), result.NewValue.ColumnCounts, result.NewValue.HoldNoteCounts);
displayXxySR.Current.Value = result.NewValue.XxySr;
}, true);
}
@@ -561,8 +434,9 @@ namespace osu.Game.Screens.Select.Carousel
// Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel.
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
keyCount = legacyRuleset.GetKeyCount(beatmapInfo, mods.Value);
keyCountText.Alpha = 1;
keyCountText.Text = cachedScratchText ?? $"[{legacyRuleset.GetKeyCount(beatmapInfo, mods.Value)}K] ";
keyCountText.Text = cachedScratchText ?? $"[{keyCount}K] ";
keyCountText.Colour = Colour4.LightPink.ToLinear();
}
else
@@ -608,12 +482,11 @@ namespace osu.Game.Screens.Select.Carousel
{
base.Dispose(isDisposing);
starDifficultyCancellationSource?.Cancel();
starDifficultyBindable?.UnbindAll();
maniaAnalysisCancellationSource?.Cancel();
starDifficultyBindable.UnbindAll();
starDifficultyBindable = null!;
maniaAnalysisCancellationSource?.Cancel();
if (maniaAnalysisBindable != null)
maniaAnalysisBindable.UnbindAll();
maniaAnalysisBindable?.UnbindAll();
maniaAnalysisBindable = null;
}
}

View File

@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -14,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
@@ -31,15 +29,14 @@ using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
{
public partial class PanelBeatmap : Panel
{
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT;
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT + 15f;
private const int mania_ui_update_throttle_ms = 15;
private const int update_ms = 15;
private StarCounter starCounter = null!;
private ConstrainedIconContainer difficultyIcon = null!;
@@ -58,11 +55,18 @@ namespace osu.Game.Screens.SelectV2
private TrianglesV2 triangles = null!;
private EzDisplayLineGraph ezKpsGraph = null!;
private EzDisplayKpsGraph ezDisplayKpsGraph = null!;
private EzKpsDisplay ezKpsDisplay = null!;
private EzKpcDisplay ezKpcDisplay = null!;
private EzDisplayXxySR displayXxySR = null!;
private Bindable<bool> xxySrFilterSetting = null!;
private IBindable<ManiaBeatmapAnalysisResult>? maniaAnalysisBindable;
private CancellationTokenSource? maniaAnalysisCancellationSource;
private Dictionary<int, int>? columnNotes;
private Dictionary<int, int>? columnLNs;
private int keyCount;
private string? scratchText;
[Resolved]
private Ez2ConfigManager ezConfig { get; set; } = null!;
@@ -87,30 +91,6 @@ namespace osu.Game.Screens.SelectV2
private BeatmapInfo beatmap => ((GroupedBeatmap)Item!.Model).Beatmap;
private IBindable<ManiaBeatmapAnalysisResult>? maniaAnalysisBindable;
private CancellationTokenSource? maniaAnalysisCancellationSource;
private string? cachedScratchText;
private ScheduledDelegate? scheduledManiaUiUpdate;
private (double averageKps, double maxKps, List<double> kpsList) pendingKpsResult;
private Dictionary<int, int>? pendingColumnCounts;
private Dictionary<int, int>? pendingHoldNoteCounts;
private bool hasPendingUiUpdate;
private Bindable<EzKpcDisplay.KpcDisplayMode> kpcDisplayMode = null!;
private int cachedKpcKeyCount = -1;
private Guid cachedKpcBeatmapId;
private int cachedKpcRulesetId = -1;
private int cachedKpcModsHash;
private Dictionary<int, int>? normalizedColumnCounts;
private Dictionary<int, int>? normalizedHoldNoteCounts;
private int normalizedCountsKeyCount;
private int lastKpcCountsHash;
private EzKpcDisplay.KpcDisplayMode lastKpcMode;
public PanelBeatmap()
{
PanelXOffset = 60;
@@ -203,22 +183,7 @@ namespace osu.Game.Screens.SelectV2
Font = OsuFont.Style.Caption1.With(weight: FontWeight.SemiBold),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft
},
ezKpsDisplay = new EzKpsDisplay
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
},
Empty(),
ezKpsGraph = new EzDisplayLineGraph
{
Size = new Vector2(300, 20),
LineColour = Color4.CornflowerBlue.Opacity(0.8f),
Blending = BlendingParameters.Mixture,
Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.CornflowerBlue),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
},
}
}
},
new FillFlowContainer
@@ -246,17 +211,34 @@ namespace osu.Game.Screens.SelectV2
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.4f)
},
ezKpcDisplay = new EzKpcDisplay
ezKpsDisplay = new EzKpsDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
ezDisplayKpsGraph = new EzDisplayKpsGraph
{
Size = new Vector2(300, 20),
Blending = BlendingParameters.Mixture,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
},
},
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 4 },
Children = new Drawable[]
{
ezKpcDisplay = new EzKpcDisplay()
}
}
}
}
}
},
}
};
}
@@ -266,32 +248,21 @@ namespace osu.Game.Screens.SelectV2
ruleset.BindValueChanged(_ =>
{
computeStarRating();
computeManiaAnalysis();
resetManiaAnalysisDisplay();
updateKeyCount();
});
mods.BindValueChanged(_ =>
{
computeStarRating();
computeManiaAnalysis();
updateKeyCount();
}, true);
// 设置 XxySRFilter 设置的绑定
xxySrFilterSetting = ezConfig.GetBindable<bool>(Ez2Setting.XxySRFilter);
xxySrFilterSetting.BindValueChanged(value =>
{
// 根据 XxySRFilter 设置切换图标
starCounter.Icon = value.NewValue
? FontAwesome.Solid.Moon
: FontAwesome.Solid.Star;
}, true); // true 表示立即触发一次以设置初始状态
kpcDisplayMode = ezConfig.GetBindable<EzKpcDisplay.KpcDisplayMode>(Ez2Setting.KpcDisplayMode);
kpcDisplayMode.BindValueChanged(mode =>
{
ezKpcDisplay.CurrentKpcDisplayMode = mode.NewValue;
}, true);
}
@@ -305,60 +276,13 @@ namespace osu.Game.Screens.SelectV2
difficultyText.Text = beatmap.DifficultyName;
authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username);
cachedScratchText = null;
resetManiaAnalysisDisplay();
computeStarRating();
computeManiaAnalysis();
computeStarRating();
updateKeyCount();
}
private Drawable getRulesetIcon(RulesetInfo rulesetInfo)
{
var rulesetInstance = rulesets.GetRuleset(rulesetInfo.ShortName)?.CreateInstance();
if (rulesetInstance is null)
return new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
return rulesetInstance.CreateIcon();
}
private static bool isPlaceholderAnalysisResult(ManiaBeatmapAnalysisResult result)
=> result.AverageKps == 0
&& result.MaxKps == 0
&& (result.KpsList.Count) == 0
&& (result.ColumnCounts.Count) == 0
&& (result.HoldNoteCounts.Count) == 0
&& string.IsNullOrEmpty(result.ScratchText)
&& result.XxySr == null;
private void queueManiaUiUpdate((double averageKps, double maxKps, List<double> kpsList) result, Dictionary<int, int>? columnCounts, Dictionary<int, int>? holdNoteCounts)
{
pendingKpsResult = result;
pendingColumnCounts = columnCounts;
pendingHoldNoteCounts = holdNoteCounts;
hasPendingUiUpdate = true;
if (scheduledManiaUiUpdate != null)
return;
scheduledManiaUiUpdate = Scheduler.AddDelayed(() =>
{
scheduledManiaUiUpdate = null;
if (!hasPendingUiUpdate)
return;
hasPendingUiUpdate = false;
updateKPs(pendingKpsResult, pendingColumnCounts, pendingHoldNoteCounts);
}, mania_ui_update_throttle_ms, false);
}
private void resetManiaAnalysisDisplay()
{
cachedScratchText = null;
displayXxySR.Current.Value = null;
if (ruleset.Value.OnlineID == 3)
{
ezKpcDisplay.Show();
@@ -371,57 +295,14 @@ namespace osu.Game.Screens.SelectV2
}
}
private void updateKPs((double averageKps, double maxKps, List<double> kpsList) result, Dictionary<int, int>? columnCounts, Dictionary<int, int>? holdNoteCounts)
private Drawable getRulesetIcon(RulesetInfo rulesetInfo)
{
if (Item == null)
return;
var rulesetInstance = rulesets.GetRuleset(rulesetInfo.ShortName)?.CreateInstance();
// 滚动过程中会有大量不可见/刚离屏的面板仍收到分析回调。
// 这些面板的 UI 更新会造成明显 GC 压力与 Draw FPS 下降,因此先缓存为 pending等再次可见时再应用。
if (Item.IsVisible != true)
{
pendingKpsResult = result;
pendingColumnCounts = columnCounts;
pendingHoldNoteCounts = holdNoteCounts;
hasPendingUiUpdate = true;
return;
}
if (rulesetInstance is null)
return new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
var (averageKps, maxKps, kpsList) = result;
ezKpsDisplay.SetKps(averageKps, maxKps);
// Update KPS graph with the KPS list
if (kpsList.Count > 0)
{
ezKpsGraph.SetValues(kpsList);
}
if (columnCounts != null)
{
// 注意:分析结果里的 ColumnCounts 只包含“出现过的列”。
// 当某个 mod 删除了某一列的所有 notes 时,这一列会缺失,
// 直接显示会导致列号错位(看起来像“没有更新”)。
// 这里把字典补齐到 0..keyCount-1缺失列填 0。
int keyCount = getCachedKpcKeyCount();
ensureNormalizedCounts(keyCount);
for (int i = 0; i < keyCount; i++)
{
normalizedColumnCounts![i] = columnCounts.GetValueOrDefault(i);
normalizedHoldNoteCounts![i] = holdNoteCounts?.GetValueOrDefault(i) ?? 0;
}
int countsHash = computeCountsHash(normalizedColumnCounts!, normalizedHoldNoteCounts!, keyCount);
var mode = ezKpcDisplay.CurrentKpcDisplayMode;
if (countsHash != lastKpcCountsHash || mode != lastKpcMode)
{
lastKpcCountsHash = countsHash;
lastKpcMode = mode;
ezKpcDisplay.UpdateColumnCounts(normalizedColumnCounts!, normalizedHoldNoteCounts!);
}
}
return rulesetInstance.CreateIcon();
}
protected override void FreeAfterUse()
@@ -434,89 +315,69 @@ namespace osu.Game.Screens.SelectV2
starDifficultyCancellationSource?.Cancel();
maniaAnalysisCancellationSource?.Cancel();
maniaAnalysisBindable = null;
cachedScratchText = null;
scratchText = null;
scheduledManiaUiUpdate?.Cancel();
scheduledManiaUiUpdate = null;
hasPendingUiUpdate = false;
pendingColumnCounts = null;
pendingHoldNoteCounts = null;
displayXxySR.Current.Value = null;
cachedKpcKeyCount = -1;
cachedKpcRulesetId = -1;
cachedKpcModsHash = 0;
normalizedColumnCounts = null;
normalizedHoldNoteCounts = null;
normalizedCountsKeyCount = 0;
lastKpcCountsHash = 0;
lastKpcMode = default;
}
private int getCachedKpcKeyCount()
{
Guid beatmapId = beatmap.ID;
int rulesetId = ruleset.Value.OnlineID;
int modsHash = computeModsHash(mods.Value);
if (cachedKpcKeyCount >= 0
&& cachedKpcBeatmapId == beatmapId
&& cachedKpcRulesetId == rulesetId
&& cachedKpcModsHash == modsHash)
return cachedKpcKeyCount;
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
cachedKpcKeyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value);
cachedKpcBeatmapId = beatmapId;
cachedKpcRulesetId = rulesetId;
cachedKpcModsHash = modsHash;
return cachedKpcKeyCount;
columnNotes = null;
columnLNs = null;
}
private void ensureNormalizedCounts(int keyCount)
{
if (normalizedColumnCounts != null && normalizedHoldNoteCounts != null && normalizedCountsKeyCount == keyCount)
if (columnNotes != null && columnLNs != null)
return;
normalizedCountsKeyCount = keyCount;
normalizedColumnCounts = new Dictionary<int, int>(keyCount);
normalizedHoldNoteCounts = new Dictionary<int, int>(keyCount);
columnNotes = new Dictionary<int, int>(keyCount);
columnLNs = new Dictionary<int, int>(keyCount);
for (int i = 0; i < keyCount; i++)
{
normalizedColumnCounts[i] = 0;
normalizedHoldNoteCounts[i] = 0;
columnNotes[i] = 0;
columnLNs[i] = 0;
}
}
private static int computeModsHash(IReadOnlyList<Mod> mods)
private void updateKPS((double averageKps, double maxKps, List<double> kpsList) result, Dictionary<int, int>? columnCounts, Dictionary<int, int>? holdNoteCounts)
{
unchecked
{
int hash = 17;
for (int i = 0; i < mods.Count; i++)
hash = hash * 31 + mods[i].GetHashCode();
if (Item == null || Item.IsVisible != true)
return;
return hash;
var (averageKps, maxKps, kpsList) = result;
ezKpsDisplay.SetKps(averageKps, maxKps);
// Update KPS graph with the KPS list
if (kpsList.Count > 0)
{
ezDisplayKpsGraph.SetPoints(kpsList);
}
if (columnCounts != null)
{
ensureNormalizedCounts(keyCount);
ezKpcDisplay.UpdateColumnCounts(columnCounts, holdNoteCounts, keyCount);
}
}
private static int computeCountsHash(Dictionary<int, int> columnCounts, Dictionary<int, int> holdCounts, int keyCount)
private void computeManiaAnalysis()
{
unchecked
maniaAnalysisCancellationSource?.Cancel();
maniaAnalysisCancellationSource = new CancellationTokenSource();
if (Item == null)
return;
maniaAnalysisBindable = maniaAnalysisCache.GetBindableAnalysis(beatmap, maniaAnalysisCancellationSource.Token, computationDelay: SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
maniaAnalysisBindable.BindValueChanged(result =>
{
int hash = 17;
// Always update scratch text and key count; apply mania UI immediately when visible.
scratchText = result.NewValue.ScratchText;
Schedule(updateKeyCount);
if (Item?.IsVisible == true)
updateKPS((result.NewValue.AverageKps, result.NewValue.MaxKps, result.NewValue.KpsList), result.NewValue.ColumnCounts, result.NewValue.HoldNoteCounts);
for (int i = 0; i < keyCount; i++)
{
hash = hash * 31 + columnCounts.GetValueOrDefault(i);
hash = hash * 31 + holdCounts.GetValueOrDefault(i);
}
return hash;
}
if (result.NewValue.XxySr != null)
displayXxySR.Current.Value = result.NewValue.XxySr;
}, true);
}
private void computeStarRating()
@@ -535,37 +396,6 @@ namespace osu.Game.Screens.SelectV2
}, true);
}
private void computeManiaAnalysis()
{
maniaAnalysisCancellationSource?.Cancel();
maniaAnalysisCancellationSource = new CancellationTokenSource();
if (Item == null)
return;
// Reset UI to avoid showing stale data from previous beatmap
// resetManiaAnalysisDisplay();
maniaAnalysisBindable = maniaAnalysisCache.GetBindableAnalysis(beatmap, maniaAnalysisCancellationSource.Token, computationDelay: SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
maniaAnalysisBindable.BindValueChanged(result =>
{
// Don't treat placeholder analysis results as real updates.
if (!isPlaceholderAnalysisResult(result.NewValue))
{
// Update cached scratch text even when it's an empty string, so the UI can reflect
// changes such as columns becoming empty. Schedule a key-count update so the
// displayed text refreshes immediately instead of waiting for a mods/ruleset change.
cachedScratchText = result.NewValue.ScratchText;
Schedule(updateKeyCount);
}
queueManiaUiUpdate((result.NewValue.AverageKps, result.NewValue.MaxKps, result.NewValue.KpsList), result.NewValue.ColumnCounts, result.NewValue.HoldNoteCounts);
if (result.NewValue.XxySr != null)
displayXxySR.Current.Value = result.NewValue.XxySr;
}, true);
}
protected override void Update()
{
base.Update();
@@ -575,18 +405,9 @@ namespace osu.Game.Screens.SelectV2
starDifficultyCancellationSource?.Cancel();
starDifficultyCancellationSource = null;
// 离屏时取消 mania 分析(其中包含 xxy_SR避免后台为不可见项占用计算预算。
maniaAnalysisCancellationSource?.Cancel();
maniaAnalysisCancellationSource = null;
}
else
{
// 重新可见时再触发一次计算
if (maniaAnalysisCancellationSource == null && Item != null)
{
computeManiaAnalysis();
}
}
// Dirty hack to make sure we don't take up spacing in parent fill flow when not displaying a rank.
// I can't find a better way to do this.
@@ -621,10 +442,10 @@ namespace osu.Game.Screens.SelectV2
// Account for mania differences locally for now.
// Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel.
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value);
keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value);
keyCountText.Alpha = 1;
keyCountText.Text = cachedScratchText ?? $"[{keyCount}K] ";
keyCountText.Text = scratchText ?? $"[{keyCount}K] ";
keyCountText.Colour = Colour4.LightPink.ToLinear();
}
else

View File

@@ -3,10 +3,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -35,10 +35,6 @@ namespace osu.Game.Screens.SelectV2
{
public const float HEIGHT = CarouselItem.DEFAULT_HEIGHT * 1.6f;
private const int mania_ui_update_throttle_ms = 100;
private const int background_load_delay_ms = 50;
private const int metadata_text_delay_ms = 30;
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
@@ -59,39 +55,23 @@ namespace osu.Game.Screens.SelectV2
#region Ez功能
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private EzBeatmapManiaAnalysisCache maniaAnalysisCache { get; set; } = null!;
private IBindable<ManiaBeatmapAnalysisResult>? maniaAnalysisBindable;
private CancellationTokenSource? maniaAnalysisCancellationSource;
private EzDisplayXxySR displayXxySR = null!;
private EzKpcDisplay ezKpcDisplay = null!;
private EzDisplayKpsGraph ezDisplayKpsGraph = null!;
private EzKpsDisplay ezKpsDisplay = null!;
private EzDisplayLineGraph ezKpsGraph = null!;
private EzKpcDisplay ezKpcDisplay = null!;
private EzDisplayXxySR displayXxySR = null!;
private IBindable<ManiaBeatmapAnalysisResult>? maniaAnalysisBindable;
private Bindable<KpcDisplayMode> kpcDisplayModeBindable = new Bindable<KpcDisplayMode>(KpcDisplayMode.BarChart);
private CancellationTokenSource? maniaAnalysisCancellationSource;
private EzKpcDisplay.KpcDisplayMode lastKpcMode;
private int lastKpcCountsHash;
private Dictionary<int, int>? columnNotes;
private Dictionary<int, int>? columnLNs;
private int keyCount;
private string? scratchText;
private bool applyNextManiaUiUpdateImmediately;
private string? cachedScratchText;
private int cachedKpcKeyCount = -1;
private Guid cachedKpcBeatmapId;
private int cachedKpcRulesetId = -1;
private int cachedKpcModsHash;
private Dictionary<int, int>? normalizedColumnCounts;
private Dictionary<int, int>? normalizedHoldNoteCounts;
private int normalizedCountsKeyCount;
private ScheduledDelegate? scheduledMetadataTextUpdate;
private ScheduledDelegate? scheduledManiaUiUpdate;
private (double averageKps, double maxKps, List<double> kpsList) pendingKpsResult;
private Dictionary<int, int>? pendingColumnCounts;
private Dictionary<int, int>? pendingHoldNoteCounts;
private bool hasPendingUiUpdate;
private const int mania_ui_update_throttle_ms = 15;
#endregion
@@ -227,12 +207,10 @@ namespace osu.Game.Screens.SelectV2
Origin = Anchor.BottomLeft,
},
Empty(),
ezKpsGraph = new EzDisplayLineGraph
ezDisplayKpsGraph = new EzDisplayKpsGraph
{
Size = new Vector2(300, 20),
LineColour = Color4.CornflowerBlue.Opacity(0.8f),
Blending = BlendingParameters.Mixture,
Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.CornflowerBlue),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
},
@@ -282,24 +260,12 @@ namespace osu.Game.Screens.SelectV2
ruleset.BindValueChanged(_ =>
{
cachedScratchText = null;
applyNextManiaUiUpdateImmediately = true;
computeManiaAnalysis();
resetManiaAnalysisDisplay();
updateKeyCount();
});
Selected.BindValueChanged(s =>
{
Expanded.Value = s.NewValue;
spreadDisplay.Enabled.Value = s.NewValue;
}, true);
mods.BindValueChanged(_ =>
{
cachedScratchText = null;
applyNextManiaUiUpdateImmediately = true;
computeManiaAnalysis();
updateKeyCount();
}, true);
@@ -308,6 +274,8 @@ namespace osu.Game.Screens.SelectV2
Expanded.Value = s.NewValue;
spreadDisplay.Enabled.Value = s.NewValue;
}, true);
kpcDisplayModeBindable.BindValueChanged(z => ezKpcDisplay.KpcDisplayMode.Value = z.NewValue, true);
}
protected override void PrepareForUse()
@@ -316,14 +284,6 @@ namespace osu.Game.Screens.SelectV2
var beatmapSet = beatmap.BeatmapSet!;
// Background/texture uploads are a major draw FPS limiter during fast scrolling.
// Delay background retrieval and only load if still visible and not pooled/reused.
beatmapBackground.Beatmap = null;
scheduleBackgroundLoad();
// Delay high-variance metadata text assignment to reduce glyph/atlas churn during fast scrolling.
scheduledMetadataTextUpdate?.Cancel();
scheduledMetadataTextUpdate = null;
scheduledBackgroundRetrieval = Scheduler.AddDelayed(b => beatmapBackground.Beatmap = beatmaps.GetWorkingBeatmap(b), beatmap, 50);
titleText.Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title);
@@ -338,122 +298,14 @@ namespace osu.Game.Screens.SelectV2
difficultyText.Text = beatmap.DifficultyName;
authorText.Text = BeatmapsetsStrings.ShowDetailsMappedBy(beatmap.Metadata.Author.Username);
cachedScratchText = null;
resetManiaAnalysisDisplay();
computeManiaAnalysis();
computeStarRating();
spreadDisplay.Beatmap.Value = beatmap;
updateKeyCount();
}
private void scheduleBackgroundLoad()
{
if (Item == null)
return;
// Only attempt to load backgrounds for currently visible panels.
if (Item.IsVisible != true)
return;
if (scheduledBackgroundRetrieval != null)
return;
Guid scheduledBeatmapId = beatmap.ID;
scheduledBackgroundRetrieval = Scheduler.AddDelayed(() =>
{
scheduledBackgroundRetrieval = null;
if (Item == null)
return;
if (Item.IsVisible != true)
return;
// Guard against pooled reuse.
if (beatmap.ID != scheduledBeatmapId)
return;
beatmapBackground.Beatmap = beatmaps.GetWorkingBeatmap(beatmap);
}, background_load_delay_ms, false);
}
private void computeManiaAnalysis()
{
maniaAnalysisCancellationSource?.Cancel();
maniaAnalysisCancellationSource = new CancellationTokenSource();
if (Item == null)
return;
// Reset UI to avoid showing stale data from previous beatmap
// resetManiaAnalysisDisplay();
maniaAnalysisBindable = maniaAnalysisCache.GetBindableAnalysis(beatmap, maniaAnalysisCancellationSource.Token, computationDelay: SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
maniaAnalysisBindable.BindValueChanged(result =>
{
if (!isPlaceholderAnalysisResult(result.NewValue))
{
// Update cached scratch text even when empty to reflect columns becoming empty.
cachedScratchText = result.NewValue.ScratchText;
Schedule(updateKeyCount);
}
queueManiaUiUpdate((result.NewValue.AverageKps, result.NewValue.MaxKps, result.NewValue.KpsList), result.NewValue.ColumnCounts, result.NewValue.HoldNoteCounts);
displayXxySR.Current.Value = result.NewValue.XxySr;
}, true);
}
private static bool isPlaceholderAnalysisResult(ManiaBeatmapAnalysisResult result)
=> result.AverageKps == 0
&& result.MaxKps == 0
&& (result.KpsList.Count) == 0
&& (result.ColumnCounts.Count) == 0
&& (result.HoldNoteCounts.Count) == 0
&& string.IsNullOrEmpty(result.ScratchText)
&& result.XxySr == null;
private void queueManiaUiUpdate((double averageKps, double maxKps, List<double> kpsList) result, Dictionary<int, int>? columnCounts, Dictionary<int, int>? holdNoteCounts)
{
// After a mod/ruleset change, apply the first incoming result immediately to avoid a visible blank window.
if (applyNextManiaUiUpdateImmediately && Item?.IsVisible == true)
{
applyNextManiaUiUpdateImmediately = false;
scheduledManiaUiUpdate?.Cancel();
scheduledManiaUiUpdate = null;
hasPendingUiUpdate = false;
updateUI(result, columnCounts, holdNoteCounts);
return;
}
pendingKpsResult = result;
pendingColumnCounts = columnCounts;
pendingHoldNoteCounts = holdNoteCounts;
hasPendingUiUpdate = true;
// Coalesce multiple incoming analysis updates into a single UI update.
if (scheduledManiaUiUpdate != null)
return;
scheduledManiaUiUpdate = Scheduler.AddDelayed(() =>
{
scheduledManiaUiUpdate = null;
if (!hasPendingUiUpdate)
return;
hasPendingUiUpdate = false;
updateUI(pendingKpsResult, pendingColumnCounts, pendingHoldNoteCounts);
}, mania_ui_update_throttle_ms, false);
}
private void resetManiaAnalysisDisplay()
{
cachedScratchText = null;
displayXxySR.Current.Value = null;
if (ruleset.Value.OnlineID == 3)
{
ezKpcDisplay.Show();
@@ -466,56 +318,6 @@ namespace osu.Game.Screens.SelectV2
}
}
private void updateUI((double averageKps, double maxKps, List<double> kpsList) result, Dictionary<int, int>? columnCounts, Dictionary<int, int>? holdNoteCounts)
{
if (Item == null)
return;
// 滚动过程中会有大量不可见/刚离屏的面板仍收到分析回调。
// 这些面板的 UI 更新会造成明显 GC 压力与 Draw FPS 下降,因此先缓存为 pending等再次可见时再应用。
if (Item.IsVisible != true)
{
pendingKpsResult = result;
pendingColumnCounts = columnCounts;
pendingHoldNoteCounts = holdNoteCounts;
hasPendingUiUpdate = true;
return;
}
var (averageKps, maxKps, kpsList) = result;
ezKpsDisplay.SetKps(averageKps, maxKps);
// Update KPS graph with the KPS list
if (kpsList.Count > 0)
{
ezKpsGraph.SetValues(kpsList);
}
if (columnCounts != null)
{
// 同 PanelBeatmap补齐缺失列为 0避免列号错位。
int keyCount = getCachedKpcKeyCount();
ensureNormalizedCounts(keyCount);
for (int i = 0; i < keyCount; i++)
{
normalizedColumnCounts![i] = columnCounts.GetValueOrDefault(i);
normalizedHoldNoteCounts![i] = holdNoteCounts?.GetValueOrDefault(i) ?? 0;
}
int countsHash = computeCountsHash(normalizedColumnCounts!, normalizedHoldNoteCounts!, keyCount);
var mode = ezKpcDisplay.CurrentKpcDisplayMode;
if (countsHash != lastKpcCountsHash || mode != lastKpcMode)
{
lastKpcCountsHash = countsHash;
lastKpcMode = mode;
ezKpcDisplay.UpdateColumnCounts(normalizedColumnCounts!, normalizedHoldNoteCounts!);
}
}
}
protected override void FreeAfterUse()
{
base.FreeAfterUse();
@@ -531,93 +333,68 @@ namespace osu.Game.Screens.SelectV2
starDifficultyCancellationSource?.Cancel();
// Ez功能
scheduledMetadataTextUpdate?.Cancel();
scheduledMetadataTextUpdate = null;
scheduledManiaUiUpdate?.Cancel();
scheduledManiaUiUpdate = null;
hasPendingUiUpdate = false;
pendingColumnCounts = null;
pendingHoldNoteCounts = null;
maniaAnalysisCancellationSource?.Cancel();
maniaAnalysisBindable = null;
cachedScratchText = null;
displayXxySR.Current.Value = null;
cachedKpcKeyCount = -1;
cachedKpcRulesetId = -1;
cachedKpcModsHash = 0;
normalizedColumnCounts = null;
normalizedHoldNoteCounts = null;
normalizedCountsKeyCount = 0;
lastKpcCountsHash = 0;
lastKpcMode = default;
}
private int getCachedKpcKeyCount()
{
Guid beatmapId = beatmap.ID;
int rulesetId = ruleset.Value.OnlineID;
int modsHash = computeModsHash(mods.Value);
if (cachedKpcKeyCount >= 0
&& cachedKpcBeatmapId == beatmapId
&& cachedKpcRulesetId == rulesetId
&& cachedKpcModsHash == modsHash)
return cachedKpcKeyCount;
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
cachedKpcKeyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value);
cachedKpcBeatmapId = beatmapId;
cachedKpcRulesetId = rulesetId;
cachedKpcModsHash = modsHash;
return cachedKpcKeyCount;
columnNotes = null;
columnLNs = null;
}
private void ensureNormalizedCounts(int keyCount)
{
if (normalizedColumnCounts != null && normalizedHoldNoteCounts != null && normalizedCountsKeyCount == keyCount)
if (columnNotes != null && columnLNs != null)
return;
normalizedCountsKeyCount = keyCount;
normalizedColumnCounts = new Dictionary<int, int>(keyCount);
normalizedHoldNoteCounts = new Dictionary<int, int>(keyCount);
columnNotes = new Dictionary<int, int>(keyCount);
columnLNs = new Dictionary<int, int>(keyCount);
for (int i = 0; i < keyCount; i++)
{
normalizedColumnCounts[i] = 0;
normalizedHoldNoteCounts[i] = 0;
columnNotes[i] = 0;
columnLNs[i] = 0;
}
}
private static int computeModsHash(IReadOnlyList<Mod> mods)
private void updateKPS((double averageKps, double maxKps, List<double> kpsList) result, Dictionary<int, int>? columnCounts, Dictionary<int, int>? holdNoteCounts)
{
unchecked
{
int hash = 17;
for (int i = 0; i < mods.Count; i++)
hash = hash * 31 + mods[i].GetHashCode();
if (Item == null || Item.IsVisible != true)
return;
return hash;
var (averageKps, maxKps, kpsList) = result;
ezKpsDisplay.SetKps(averageKps, maxKps);
if (kpsList.Count > 0)
{
ezDisplayKpsGraph.SetPoints(kpsList);
}
if (columnCounts != null)
{
ensureNormalizedCounts(keyCount);
ezKpcDisplay.UpdateColumnCounts(columnCounts, holdNoteCounts, keyCount);
}
}
private static int computeCountsHash(Dictionary<int, int> columnCounts, Dictionary<int, int> holdCounts, int keyCount)
private void computeManiaAnalysis()
{
unchecked
maniaAnalysisCancellationSource?.Cancel();
maniaAnalysisCancellationSource = new CancellationTokenSource();
if (Item == null)
return;
maniaAnalysisBindable = maniaAnalysisCache.GetBindableAnalysis(beatmap, maniaAnalysisCancellationSource.Token, computationDelay: SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
maniaAnalysisBindable.BindValueChanged(result =>
{
int hash = 17;
// Always update scratch text and key count; apply mania UI immediately when visible.
scratchText = result.NewValue.ScratchText;
Schedule(updateKeyCount);
if (Item?.IsVisible == true)
updateKPS((result.NewValue.AverageKps, result.NewValue.MaxKps, result.NewValue.KpsList), result.NewValue.ColumnCounts, result.NewValue.HoldNoteCounts);
for (int i = 0; i < keyCount; i++)
{
hash = hash * 31 + columnCounts.GetValueOrDefault(i);
hash = hash * 31 + holdCounts.GetValueOrDefault(i);
}
return hash;
}
if (result.NewValue.XxySr != null)
displayXxySR.Current.Value = result.NewValue.XxySr;
}, true);
}
private void computeStarRating()
@@ -645,44 +422,9 @@ namespace osu.Game.Screens.SelectV2
starDifficultyCancellationSource?.Cancel();
starDifficultyCancellationSource = null;
// 离屏取消,避免后台为不可见项占用计算预算。
scheduledBackgroundRetrieval?.Cancel();
scheduledBackgroundRetrieval = null;
scheduledMetadataTextUpdate?.Cancel();
scheduledMetadataTextUpdate = null;
maniaAnalysisCancellationSource?.Cancel();
maniaAnalysisCancellationSource = null;
}
else
{
if (beatmapBackground.Beatmap == null)
scheduleBackgroundLoad();
// 重新可见时再触发一次绑定/计算。
if (maniaAnalysisCancellationSource == null && Item != null)
{
// 离屏期间我们会 cancel 掉分析(避免浪费计算预算)。
// 重新变为可见时,必须先清空旧显示值,否则会短暂显示上一次谱面的结果(表现为 xxySR 跳变)。
resetManiaAnalysisDisplay();
computeManiaAnalysis();
}
// 如果离屏期间收到过分析结果(或刚好在离屏时更新被跳过),这里补一次 UI 应用。
if (hasPendingUiUpdate && scheduledManiaUiUpdate == null)
{
scheduledManiaUiUpdate = Scheduler.AddDelayed(() =>
{
scheduledManiaUiUpdate = null;
if (!hasPendingUiUpdate)
return;
hasPendingUiUpdate = false;
updateUI(pendingKpsResult, pendingColumnCounts, pendingHoldNoteCounts);
}, 0, false);
}
}
// Dirty hack to make sure we don't take up spacing in parent fill flow when not displaying a rank.
// I can't find a better way to do this.
@@ -707,10 +449,10 @@ namespace osu.Game.Screens.SelectV2
// Account for mania differences locally for now.
// Eventually this should be handled in a more modular way, allowing rulesets to add more information to the panel.
ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
int keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value);
keyCount = legacyRuleset.GetKeyCount(beatmap, mods.Value);
keyCountText.Alpha = 1;
keyCountText.Text = cachedScratchText ?? $"[{keyCount}K] ";
keyCountText.Text = scratchText ?? $"[{keyCount}K] ";
keyCountText.Colour = Colour4.LightPink.ToLinear();
}
else

View File

@@ -1,15 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.LAsEzExtensions.Configuration;
using osuTK;
using osuTK.Graphics;
@@ -17,71 +21,44 @@ namespace osu.Game.Screens.SelectV2
{
public partial class EzKpcDisplay : CompositeDrawable
{
/// <summary>
/// 显示模式枚举
/// </summary>
public enum KpcDisplayMode
{
/// <summary>
/// 数字(默认,最高性能)
/// </summary>
Numbers,
public Bindable<KpcDisplayMode> KpcDisplayMode { get; } = new Bindable<KpcDisplayMode>(SelectV2.KpcDisplayMode.BarChart);
/// <summary>
/// 柱状图
/// </summary>
BarChart
}
/// <summary>
/// Maximum bar height in pixels. Can be set externally.
/// Default aligned to SpreadDisplay max height.
/// </summary>
public float MaxBarHeight { get; set; } = 6f;
private readonly FillFlowContainer columnNotesContainer;
private KpcDisplayMode currentKpcDisplayMode = KpcDisplayMode.Numbers;
private Dictionary<int, int>? currentColumnCounts;
private Dictionary<int, int>? currentHoldNoteCounts;
private readonly OsuSpriteText? headerText;
private readonly Box backgroundBox;
private int currentColumnCount;
private bool modeChanged;
private readonly List<NumberColumnEntry> numberEntries = new List<NumberColumnEntry>();
private readonly List<BarChartColumnEntry> barEntries = new List<BarChartColumnEntry>();
/// <summary>
/// 当前显示模式
/// </summary>
public KpcDisplayMode CurrentKpcDisplayMode
{
get => currentKpcDisplayMode;
set
{
if (currentKpcDisplayMode == value)
return;
currentKpcDisplayMode = value;
// 如果有数据,立即重新渲染
if (currentColumnCounts != null)
{
updateDisplay(currentColumnCounts, currentHoldNoteCounts);
}
}
}
[Resolved]
private Ez2ConfigManager ezConfig { get; set; } = null!;
public EzKpcDisplay()
{
AutoSizeAxes = Axes.Both;
InternalChild = new CircularContainer
InternalChild = new Container
{
Masking = true,
CornerRadius = 5,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
backgroundBox = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Black.Opacity(0.6f),
},
new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Horizontal = 8f },
ColumnDimensions = new[]
@@ -95,7 +72,7 @@ namespace osu.Game.Screens.SelectV2
{
new[]
{
new OsuSpriteText
headerText = new OsuSpriteText
{
Text = "[Notes]",
Font = OsuFont.GetFont(size: 14),
@@ -108,6 +85,7 @@ namespace osu.Game.Screens.SelectV2
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5), // align spacing with SpreadDisplay
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
@@ -118,38 +96,131 @@ namespace osu.Game.Screens.SelectV2
};
}
protected override void LoadComplete()
{
base.LoadComplete();
FinishTransforms(true);
}
[BackgroundDependencyLoader]
private void load()
{
// Initialize bindable from config and hook up visibility/builder logic here (same lifecycle as SpreadDisplay).
KpcDisplayMode.Value = ezConfig.GetBindable<KpcDisplayMode>(Ez2Setting.KpcDisplayMode).Value;
// Ensure we rebuild entries when the display mode changes and toggle header visibility.
KpcDisplayMode.BindValueChanged(mode =>
{
modeChanged = true;
if (headerText != null)
{
if (mode.NewValue == SelectV2.KpcDisplayMode.Numbers)
headerText.Show();
else
headerText.Hide();
}
// Adjust background to match SpreadDisplay-like style in BarChart mode.
if (backgroundBox != null)
{
if (mode.NewValue == SelectV2.KpcDisplayMode.BarChart)
{
backgroundBox.Colour = Colour4.White;
backgroundBox.Alpha = 0.06f;
}
else
{
backgroundBox.Colour = Colour4.Black;
backgroundBox.Alpha = 0.6f;
}
}
}, false);
// Initial header visibility based on current mode.
if (headerText != null)
{
if (KpcDisplayMode.Value == SelectV2.KpcDisplayMode.Numbers)
headerText.Show();
else
headerText.Hide();
}
// Apply initial background style based on current mode.
if (backgroundBox != null)
{
if (KpcDisplayMode.Value == SelectV2.KpcDisplayMode.BarChart)
{
backgroundBox.Colour = Colour4.White;
backgroundBox.Alpha = 0.06f;
}
else
{
backgroundBox.Colour = Colour4.Black;
backgroundBox.Alpha = 0.6f;
}
}
}
/// <summary>
/// 更新列音符数量显示
/// </summary>
/// <param name="columnNoteCounts">每列的音符数量</param>
/// <param name="holdNoteCounts">面条数量</param>
public void UpdateColumnCounts(Dictionary<int, int> columnNoteCounts, Dictionary<int, int>? holdNoteCounts = null)
/// <param name="keyCount"></param>
public void UpdateColumnCounts(Dictionary<int, int> columnNoteCounts, Dictionary<int, int>? holdNoteCounts = null, int? keyCount = null)
{
currentColumnCounts = columnNoteCounts;
currentHoldNoteCounts = holdNoteCounts;
if (keyCount.HasValue)
{
int kc = keyCount.Value;
var normalized = new Dictionary<int, int>(Math.Max(0, kc));
var normalizedHold = new Dictionary<int, int>(Math.Max(0, kc));
for (int i = 0; i < kc; i++)
{
normalized[i] = columnNoteCounts.GetValueOrDefault(i);
normalizedHold[i] = holdNoteCounts?.GetValueOrDefault(i) ?? 0;
}
columnNoteCounts = normalized;
holdNoteCounts = normalizedHold;
}
else
{
// When keyCount is not provided, normalize sparse key dictionaries into
// a continuous 0..maxKey range so callers can index by column.
if (columnNoteCounts.Count > 0)
{
int maxKey = 0;
foreach (var k in columnNoteCounts.Keys)
if (k > maxKey) maxKey = k;
int kc = maxKey + 1;
var normalized = new Dictionary<int, int>(Math.Max(0, kc));
var normalizedHold = new Dictionary<int, int>(Math.Max(0, kc));
for (int i = 0; i < kc; i++)
{
normalized[i] = columnNoteCounts.GetValueOrDefault(i);
normalizedHold[i] = holdNoteCounts?.GetValueOrDefault(i) ?? 0;
}
columnNoteCounts = normalized;
holdNoteCounts = normalizedHold;
}
}
updateDisplay(columnNoteCounts, holdNoteCounts);
}
private void updateDisplay(Dictionary<int, int> columnNoteCounts, Dictionary<int, int>? holdNoteCounts = null)
{
int columns = columnNoteCounts.Count;
if (columns == 0)
switch (KpcDisplayMode.Value)
{
currentColumnCount = 0;
columnNotesContainer.Clear();
numberEntries.Clear();
barEntries.Clear();
return;
}
switch (currentKpcDisplayMode)
{
case KpcDisplayMode.Numbers:
case SelectV2.KpcDisplayMode.Numbers:
updateNumbersDisplay(columnNoteCounts, holdNoteCounts);
break;
case KpcDisplayMode.BarChart:
case SelectV2.KpcDisplayMode.BarChart:
updateBarChartDisplay(columnNoteCounts, holdNoteCounts);
break;
}
@@ -157,65 +228,104 @@ namespace osu.Game.Screens.SelectV2
private void rebuildForModeIfNeeded(int columns)
{
// If mode changed, recreate entries for the new mode (mode changes are rare).
if (modeChanged)
{
currentColumnCount = 0;
columnNotesContainer.Clear();
numberEntries.Clear();
barEntries.Clear();
modeChanged = false;
}
if (currentColumnCount == columns)
return;
currentColumnCount = columns;
columnNotesContainer.Clear();
numberEntries.Clear();
barEntries.Clear();
switch (currentKpcDisplayMode)
// If increasing column count, add new entries. If decreasing, hide extras.
if (columns > currentColumnCount)
{
case KpcDisplayMode.Numbers:
for (int i = 0; i < columns; i++)
{
var entry = new NumberColumnEntry(i);
numberEntries.Add(entry);
columnNotesContainer.Add(entry.Container);
}
switch (KpcDisplayMode.Value)
{
case SelectV2.KpcDisplayMode.Numbers:
for (int i = currentColumnCount; i < columns; i++)
{
var entry = new NumberColumnEntry(i);
numberEntries.Add(entry);
columnNotesContainer.Add(entry.Container);
}
break;
break;
case KpcDisplayMode.BarChart:
for (int i = 0; i < columns; i++)
{
var entry = new BarChartColumnEntry(i);
barEntries.Add(entry);
columnNotesContainer.Add(entry.Container);
}
case SelectV2.KpcDisplayMode.BarChart:
for (int i = currentColumnCount; i < columns; i++)
{
var entry = new BarChartColumnEntry(i, MaxBarHeight, false);
barEntries.Add(entry);
columnNotesContainer.Add(entry.Container);
}
break;
break;
}
}
else
{
switch (KpcDisplayMode.Value)
{
case SelectV2.KpcDisplayMode.Numbers:
for (int i = columns; i < numberEntries.Count; i++)
numberEntries[i].Container.Hide();
break;
case SelectV2.KpcDisplayMode.BarChart:
for (int i = columns; i < barEntries.Count; i++)
barEntries[i].Container.Hide();
break;
}
}
currentColumnCount = columns;
}
private void updateNumbersDisplay(Dictionary<int, int> columnNoteCounts, Dictionary<int, int>? holdNoteCounts = null)
{
rebuildForModeIfNeeded(columnNoteCounts.Count);
// Expect 0..N-1 keys (PanelBeatmap normalizes). Fall back to ordered keys if needed.
if (columnNoteCounts.Count == numberEntries.Count && columnNoteCounts.ContainsKey(0))
int visible = currentColumnCount;
// 必须使用方法补0
if (columnNoteCounts.Count >= visible && columnNoteCounts.ContainsKey(0))
{
for (int i = 0; i < numberEntries.Count; i++)
for (int i = 0; i < visible; i++)
{
int total = columnNoteCounts.GetValueOrDefault(i);
int hold = holdNoteCounts?.GetValueOrDefault(i) ?? 0;
numberEntries[i].Container.Show();
numberEntries[i].SetValues(total, hold);
}
return;
}
else
int idx = 0;
foreach (var kvp in columnNoteCounts.OrderBy(k => k.Key))
{
int idx = 0;
if (idx >= visible)
break;
foreach (var kvp in columnNoteCounts.OrderBy(k => k.Key))
{
if (idx >= numberEntries.Count)
break;
int hold = holdNoteCounts?.GetValueOrDefault(kvp.Key) ?? 0;
numberEntries[idx].Container.Show();
numberEntries[idx].SetValues(kvp.Value, hold);
idx++;
}
int hold = holdNoteCounts?.GetValueOrDefault(kvp.Key) ?? 0;
numberEntries[idx].SetValues(kvp.Value, hold);
idx++;
}
// Zero-fill remaining visible slots.
for (int i = idx; i < visible; i++)
{
numberEntries[i].Container.Show();
numberEntries[i].SetValues(0, 0);
}
}
@@ -223,9 +333,11 @@ namespace osu.Game.Screens.SelectV2
{
rebuildForModeIfNeeded(columnNoteCounts.Count);
int visible = currentColumnCount;
int maxCount = 0;
for (int i = 0; i < currentColumnCount; i++)
for (int i = 0; i < visible; i++)
{
int total = columnNoteCounts.GetValueOrDefault(i);
int hold = holdNoteCounts?.GetValueOrDefault(i) ?? 0;
@@ -236,36 +348,29 @@ namespace osu.Game.Screens.SelectV2
if (maxCount == 0)
{
// Nothing to show.
for (int i = 0; i < barEntries.Count; i++)
// Nothing to show: zero-fill visible slots.
for (int i = 0; i < visible; i++)
barEntries[i].Container.Show();
for (int i = 0; i < visible; i++)
barEntries[i].SetValues(0, 0, 1);
return;
}
for (int i = 0; i < barEntries.Count; i++)
for (int i = 0; i < visible; i++)
{
int total = columnNoteCounts.GetValueOrDefault(i);
int hold = holdNoteCounts?.GetValueOrDefault(i) ?? 0;
barEntries[i].Container.Show();
barEntries[i].SetValues(total, hold, maxCount);
}
}
/// <summary>
/// 清空显示
/// </summary>
public void Clear()
{
currentColumnCounts = null;
columnNotesContainer.Clear();
numberEntries.Clear();
barEntries.Clear();
currentColumnCount = 0;
}
private class NumberColumnEntry
{
public readonly FillFlowContainer Container;
private readonly OsuSpriteText valueText;
private readonly OsuSpriteText? valueText;
private readonly OsuSpriteText holdText;
private int lastTotal = int.MinValue;
@@ -306,7 +411,7 @@ namespace osu.Game.Screens.SelectV2
if (lastTotal != total)
{
lastTotal = total;
valueText.Text = total.ToString(CultureInfo.InvariantCulture);
if (valueText != null) valueText.Text = total.ToString(CultureInfo.InvariantCulture);
}
if (lastHold != hold)
@@ -319,67 +424,81 @@ namespace osu.Game.Screens.SelectV2
private class BarChartColumnEntry
{
private const float max_bar_height = 30f;
private const float bar_width = 20f;
private const float bar_spacing = 2f;
// Align width/spacing with SpreadDisplay maximums for consistent visuals and layout.
private const float bar_width = 7f;
private const float bar_spacing = 5f;
private readonly float maxBarHeight;
private static readonly Color4 hold_note_color = Color4Extensions.FromHex("#FFD39B");
public readonly Container Container;
private readonly Box regularBox;
private readonly Box holdBox;
private readonly OsuSpriteText valueText;
private readonly OsuSpriteText? valueText;
private int lastTotalNotes = int.MinValue;
private int lastHoldNotes = int.MinValue;
private int lastMaxCount = int.MinValue;
public BarChartColumnEntry(int index)
public BarChartColumnEntry(int index, float maxBarHeight, bool showText = false)
{
this.maxBarHeight = maxBarHeight;
// Match SpreadDisplay's largest dot: width 7, height 12 (maxBarHeight).
Container = new Container
{
Size = new Vector2(bar_width, max_bar_height + 20),
Size = new Vector2(bar_width, this.maxBarHeight),
Margin = new MarginPadding { Right = bar_spacing },
};
// Create a masked bar area with corner radius to achieve capsule appearance.
var barArea = new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Size = new Vector2(bar_width, this.maxBarHeight),
Margin = new MarginPadding { Bottom = 0 },
Masking = true,
CornerRadius = bar_width / 2f, // pill shape: radius = half width (matches reference)
};
regularBox = new Box
{
RelativeSizeAxes = Axes.X,
Colour = Color4.LightCoral,
Height = 0,
Colour = Color4Extensions.FromHex("#4DA6FF"), // regular notes: blue
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Margin = new MarginPadding { Bottom = 15 }
};
holdBox = new Box
{
RelativeSizeAxes = Axes.X,
Height = 0,
Colour = hold_note_color,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
};
valueText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 10),
Colour = Color4.White,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
};
barArea.AddRange(new Drawable[] { regularBox, holdBox });
Container.Children = new Drawable[]
// Build children list and only create text drawables when requested to avoid allocations/layout.
var children = new List<Drawable>
{
new Box
{
RelativeSizeAxes = Axes.X,
Height = max_bar_height,
Height = this.maxBarHeight,
Colour = Color4.Gray.Opacity(0.2f),
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Margin = new MarginPadding { Bottom = 15 }
Margin = new MarginPadding { Bottom = 0 }
},
regularBox,
holdBox,
new OsuSpriteText
barArea,
};
if (showText)
{
var idxText = new OsuSpriteText
{
Text = (index + 1).ToString(),
Font = OsuFont.GetFont(size: 12),
@@ -387,9 +506,21 @@ namespace osu.Game.Screens.SelectV2
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Margin = new MarginPadding { Bottom = 2 }
},
valueText,
};
};
valueText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 10),
Colour = Color4.White,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
};
children.Add(idxText);
children.Add(valueText);
}
Container.Children = children.ToArray();
}
public void SetValues(int totalNotes, int holdNotes, int maxCount)
@@ -404,19 +535,42 @@ namespace osu.Game.Screens.SelectV2
int regularNotes = totalNotes - holdNotes;
float totalHeight = maxCount > 0 ? (float)totalNotes / maxCount * max_bar_height : 0;
float regularHeight = maxCount > 0 ? (float)regularNotes / maxCount * max_bar_height : 0;
float totalHeight = maxCount > 0 ? (float)totalNotes / maxCount * maxBarHeight : 0;
float regularHeight = maxCount > 0 ? (float)regularNotes / maxCount * maxBarHeight : 0;
regularBox.Height = regularHeight;
// Smoothly animate height/margin/text changes to avoid visual jitter when data updates.
const float transition_duration = 200f;
regularBox.ResizeHeightTo(regularHeight, transition_duration, Easing.OutQuint);
holdBox.Height = totalHeight - regularHeight;
holdBox.Margin = new MarginPadding { Bottom = 15 + regularHeight };
float holdHeight = Math.Max(0, totalHeight - regularHeight);
holdBox.ResizeHeightTo(holdHeight, transition_duration, Easing.OutQuint);
// place holdBox above regular by animating its bottom margin inside the masked bar area
holdBox.TransformTo(nameof(Drawable.Margin), new MarginPadding { Bottom = regularHeight }, transition_duration, Easing.OutQuint);
valueText.Text = holdNotes > 0
? $"{totalNotes.ToString(CultureInfo.InvariantCulture)}({holdNotes.ToString(CultureInfo.InvariantCulture)})"
: totalNotes.ToString(CultureInfo.InvariantCulture);
valueText.Y = -(totalHeight + 17);
if (valueText != null)
{
valueText.Text = holdNotes > 0
? $"{totalNotes.ToString(CultureInfo.InvariantCulture)}({holdNotes.ToString(CultureInfo.InvariantCulture)})"
: totalNotes.ToString(CultureInfo.InvariantCulture);
valueText.MoveToY(-(totalHeight + 6), transition_duration, Easing.OutQuint);
}
}
}
}
/// <summary>
/// 显示模式枚举
/// </summary>
public enum KpcDisplayMode
{
/// <summary>
/// 数字(默认,最高性能)
/// </summary>
Numbers,
/// <summary>
/// 柱状图
/// </summary>
BarChart
}
}

View File

@@ -1,21 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.LAsEzExtensions;
using osu.Game.LAsEzExtensions.Analysis;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK.Graphics;
namespace osu.Game.Screens.SelectV2
@@ -23,20 +13,6 @@ namespace osu.Game.Screens.SelectV2
public partial class EzKpsDisplay : CompositeDrawable
{
private readonly OsuSpriteText kpsText;
// private readonly LineGraph kpsGraph;
public static bool KpsCacheEnabled = true;
private const int max_kps_cache_entries = 24;
private readonly Dictionary<string, (double averageKps, double maxKps, List<double> kpsList)> kpsCache = new Dictionary<string, (double, double, List<double>)>();
private readonly Queue<string> kpsCacheInsertionOrder = new Queue<string>();
private CancellationTokenSource? calculationCancellationSource;
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
public event Action<Dictionary<int, int>>? ColumnCountsUpdated;
public event Action<IBeatmap?>? BeatmapUpdated;
public EzKpsDisplay()
{
@@ -49,140 +25,6 @@ namespace osu.Game.Screens.SelectV2
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
};
// 如果需要图表显示,可以取消注释
// InternalChildren = new Drawable[]
// {
// kpsText = new OsuSpriteText
// {
// Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
// Colour = Color4.CornflowerBlue,
// Anchor = Anchor.CentreLeft,
// Origin = Anchor.CentreLeft
// },
// kpsGraph = new LineGraph
// {
// Size = new Vector2(300, 20),
// Colour = OsuColour.Gray(0.25f),
// Anchor = Anchor.CentreLeft,
// Origin = Anchor.CentreLeft,
// Margin = new MarginPadding { Left = 100 }
// },
// };
}
/// <summary>
/// 异步计算并显示KPS信息
/// </summary>
/// <param name="beatmapInfo">谱面信息</param>
/// <param name="ruleset">规则集</param>
/// <param name="mods">模组列表</param>
public void UpdateKpsAsync(BeatmapInfo beatmapInfo, RulesetInfo ruleset, IReadOnlyList<Mod>? mods)
{
if (ruleset.OnlineID != 3) // 只在Mania模式下显示
{
Hide();
return;
}
Show();
// 取消之前的计算
calculationCancellationSource?.Cancel();
calculationCancellationSource = new CancellationTokenSource();
var cancellationToken = calculationCancellationSource.Token;
// 生成缓存键
string cacheKey = mods == null
? $"{beatmapInfo.Hash}"
: ManiaBeatmapAnalysisCache.CreateCacheKey(beatmapInfo, ruleset, mods);
// 检查缓存(可开关,便于对比测试)
if (KpsCacheEnabled && kpsCache.TryGetValue(cacheKey, out var cachedResult))
{
updateUI(cachedResult, null, null);
return;
}
// 显示计算中状态
kpsText.Text = " KPS: calculating...";
// 异步计算
Task.Run(() =>
{
try
{
var workingBeatmap = beatmapManager.GetWorkingBeatmap(beatmapInfo);
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset, mods, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
// 使用优化后的计算器一次性获取所有数据
var (averageKps, maxKps, kpsList, columnCounts, holdNoteCounts) = OptimizedBeatmapCalculator.GetAllDataOptimized(playableBeatmap);
var kpsResult = (averageKps, maxKps, kpsList);
cancellationToken.ThrowIfCancellationRequested();
// 缓存结果
if (KpsCacheEnabled)
{
kpsCache[cacheKey] = kpsResult;
kpsCacheInsertionOrder.Enqueue(cacheKey);
while (kpsCache.Count > max_kps_cache_entries && kpsCacheInsertionOrder.Count > 0)
{
string oldest = kpsCacheInsertionOrder.Dequeue();
kpsCache.Remove(oldest);
}
}
// 在UI线程中更新界面
Schedule(() =>
{
if (!cancellationToken.IsCancellationRequested)
{
updateUI(kpsResult, columnCounts, playableBeatmap);
}
});
}
catch (OperationCanceledException)
{
// 计算被取消,忽略
}
catch (Exception)
{
// 计算出错,显示默认值
Schedule(() =>
{
if (!cancellationToken.IsCancellationRequested)
{
updateUI((0, 0, new List<double>()), null, null);
}
});
}
}, cancellationToken);
}
private void updateUI((double averageKps, double maxKps, List<double> kpsList) kpsResult,
Dictionary<int, int>? columnCounts,
IBeatmap? beatmap)
{
var (averageKps, maxKps, _) = kpsResult;
// 更新KPS文本
kpsText.Text = averageKps > 0 ? $" KPS: {averageKps:F1} ({maxKps:F1} Max)" : " KPS: calculating...";
// 更新图表(如果启用)
// kpsGraph.Values = kpsList.Count > 0 ? kpsList.Select(kps => (float)kps).ToArray() : new[] { 0f };
// 通知外部组件列数据已更新
if (columnCounts != null)
{
ColumnCountsUpdated?.Invoke(columnCounts);
}
// 通知外部组件beatmap已更新
BeatmapUpdated?.Invoke(beatmap);
}
/// <summary>
@@ -192,23 +34,7 @@ namespace osu.Game.Screens.SelectV2
/// <param name="maxKps">最大KPS</param>
public void SetKps(double averageKps, double maxKps)
{
kpsText.Text = averageKps > 0 ? $" KPS: {averageKps:F1} ({maxKps:F1} Max)" : " KPS: calculating...";
}
/// <summary>
/// 清空显示并取消计算
/// </summary>
public void Clear()
{
calculationCancellationSource?.Cancel();
kpsText.Text = string.Empty;
Hide();
}
protected override void Dispose(bool isDisposing)
{
calculationCancellationSource?.Cancel();
base.Dispose(isDisposing);
kpsText.Text = averageKps > 0 ? $" [KPS] {averageKps:F1} ({maxKps:F1} Max)" : "";
}
}
}

View File

@@ -366,6 +366,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JIT/@EntryIndexedValue">JIT</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=KDDK/@EntryIndexedValue">KDDK</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=KKDD/@EntryIndexedValue">KKDD</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=KPS/@EntryIndexedValue">KPS</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LN/@EntryIndexedValue">LN</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LR/@EntryIndexedValue">LR</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LTRB/@EntryIndexedValue">LTRB</s:String>