mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-15 03:20:27 +00:00
[逐步优化]修改性能分析控件加载逻辑
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, "");
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)" : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user