Files
Ez2Lazer/osu.Game/LAsEzExtensions/EzLocalTextureFactory.cs
LA 0b9f9f70d6 主要为代码质量更新
1. 匹配新版按钮控件的自动宽度写法

2. 统一Ez日志写入方向

3.移除历史修改:缓存启用mod列表,切换mod时保持通用mod开启状态

4.代码格式化、

5.修改文件名称表意,更直观
2026-03-12 19:29:55 +08:00

540 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.Concurrent;
using System.Collections.Generic;
using System.Text;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.LAsEzExtensions.Analysis;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.LAsEzExtensions
{
[Cached]
public partial class EzLocalTextureFactory : CompositeDrawable
{
// private const int max_stage_frames = 120;
private const int max_frames_to_load = 240;
private const double default_frame_length = 1000.0 / 60.0 * 4;
private const float default_stage_body_height = 247f;
private const float square_ratio_threshold = 0.75f;
private static readonly object cleanup_lock = new object();
private static readonly ConcurrentDictionary<string, CacheEntry> global_cache = new ConcurrentDictionary<string, CacheEntry>();
private static readonly ConcurrentDictionary<string, float> note_ratio_cache = new ConcurrentDictionary<string, float>();
private readonly Dictionary<string, TextureLoaderStore> loaderStoreCache = new Dictionary<string, TextureLoaderStore>();
private readonly Ez2ConfigManager ezSkinConfig;
private readonly LargeTextureStore stageTextureStore;
private readonly TextureStore textureStore;
private readonly Bindable<string> noteSetName;
private readonly Bindable<string> stageName;
private readonly Bindable<double> columnWidth;
private readonly Bindable<double> specialFactor;
private readonly Bindable<double> hitPositonBindable;
private readonly Bindable<double> noteHeightScaleToWidth;
// scheduling guard to batch multiple bindable changes into one refresh
private bool textureRefreshScheduled;
private Vector2 tempNoteSize;
private readonly struct CacheEntry : IEquatable<CacheEntry>
{
public readonly List<Texture>? Textures;
public readonly bool HasComponent;
public readonly DateTime LastAccess;
public CacheEntry(List<Texture>? textures, bool hasComponent)
{
Textures = textures;
HasComponent = hasComponent;
LastAccess = DateTime.UtcNow;
}
public CacheEntry UpdateAccess() => new CacheEntry(Textures, HasComponent);
public bool Equals(CacheEntry other) => ReferenceEquals(Textures, other.Textures) &&
HasComponent == other.HasComponent &&
LastAccess.Equals(other.LastAccess);
public override bool Equals(object? obj) => obj is CacheEntry other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Textures, HasComponent, LastAccess);
}
public EzLocalTextureFactory(Ez2ConfigManager ezSkinConfig,
IRenderer renderer,
Storage hostStorage)
{
this.ezSkinConfig = ezSkinConfig;
const string base_path = "EzResources/";
if (!loaderStoreCache.TryGetValue(base_path, out var baseTextureLoaderStore))
{
var baseStorage = hostStorage.GetStorageForDirectory(base_path);
var baseFileStore = new StorageBackedResourceStore(baseStorage);
baseTextureLoaderStore = new TextureLoaderStore(baseFileStore);
loaderStoreCache[base_path] = baseTextureLoaderStore;
}
// 创建尺寸限制的纹理加载器(使用官方的 MaxDimensionLimitedTextureLoaderStore
var limitedLoader = new MaxDimensionLimitedTextureLoaderStore(baseTextureLoaderStore);
textureStore = new TextureStore(renderer, limitedLoader);
textureStore.AddTextureSource(baseTextureLoaderStore);
stageTextureStore = new LargeTextureStore(renderer, limitedLoader);
stageTextureStore.AddTextureSource(baseTextureLoaderStore);
noteSetName = ezSkinConfig.GetBindable<string>(Ez2Setting.NoteSetName);
stageName = ezSkinConfig.GetBindable<string>(Ez2Setting.StageName);
columnWidth = ezSkinConfig.GetBindable<double>(Ez2Setting.ColumnWidth);
specialFactor = ezSkinConfig.GetBindable<double>(Ez2Setting.SpecialFactor);
hitPositonBindable = ezSkinConfig.GetBindable<double>(Ez2Setting.HitPosition);
noteHeightScaleToWidth = ezSkinConfig.GetBindable<double>(Ez2Setting.NoteHeightScaleToWidth);
// Batch refresh: subscribe once and schedule a single refresh per frame when any relevant config changes.
void scheduleTextureRefresh()
{
if (textureRefreshScheduled) return;
textureRefreshScheduled = true;
Schedule(() =>
{
textureRefreshScheduled = false;
// Clear caches and let consumers lazily reload textures on next access.
ClearGlobalCache();
lock (loaderStoreCache)
loaderStoreCache.Clear();
});
}
// Subscribe and reuse the same schedule lambda for different generic Bindable<T>
noteSetName.BindValueChanged(_ => scheduleTextureRefresh(), true);
stageName.BindValueChanged(_ => scheduleTextureRefresh(), true);
columnWidth.BindValueChanged(_ => scheduleTextureRefresh(), true);
specialFactor.BindValueChanged(_ => scheduleTextureRefresh(), true);
hitPositonBindable.BindValueChanged(_ => scheduleTextureRefresh(), true);
noteHeightScaleToWidth.BindValueChanged(_ => scheduleTextureRefresh(), true);
}
#region
// public bool IsSquareNote(string component) => GetRatio(component) >= square_ratio_threshold;
public float GetRatio()
{
string noteSet = noteSetName.Value;
float ratio = note_ratio_cache.GetOrAdd(noteSet, ns =>
{
string path = getComponentPath(ns, "whitenote"); // Use a representative component
float calculatedRatio = calculateRatio(path);
return calculatedRatio >= square_ratio_threshold ? 1.0f : calculatedRatio;
});
return ratio;
}
public Bindable<Vector2> GetNoteSize(int keyMode, int columnIndex, bool? noSpecial = null)
{
var result = new Bindable<Vector2>();
bool isSpecialColumn = noSpecial != true && ezSkinConfig.IsSpecialColumnFast(keyMode, columnIndex);
void updateNoteSize()
{
float ratio = GetRatio();
float x = (float)(columnWidth.Value * (isSpecialColumn ? specialFactor.Value : 1.0));
float y = (float)noteHeightScaleToWidth.Value * ratio * x;
tempNoteSize.X = x;
tempNoteSize.Y = y;
result.Value = tempNoteSize;
}
columnWidth.BindValueChanged(_ => updateNoteSize());
specialFactor.BindValueChanged(_ => updateNoteSize());
noteHeightScaleToWidth.BindValueChanged(_ => updateNoteSize());
noteSetName.BindValueChanged(_ => updateNoteSize());
updateNoteSize();
return result;
}
private string getComponentPath(string noteName, string component)
{
return $"note/{noteName}/{component}";
}
private float calculateRatio(string path)
{
try
{
if (global_cache.TryGetValue(path, out var cacheEntry) &&
cacheEntry.Textures is not null && cacheEntry.Textures.Count > 0)
{
var firstFrame = cacheEntry.Textures[0];
return firstFrame.Height / (float)firstFrame.Width;
}
var sb = new StringBuilder(path.Length + 8);
Texture? texture = textureStore.Get(sb.Append(path).Append("/000.png").ToString()) ??
textureStore.Get(sb.Clear().Append(path).Append("/001.png").ToString());
return texture?.Height / (texture?.Width ?? 1f) ?? 1.0f;
}
catch (Exception ex)
{
Logger.Log($"Error calculating ratio for {path}: {ex.Message}",
LoggingTarget.Runtime, LogLevel.Error);
return 1.0f;
}
}
private static bool isStageTexturePath(string texturePath)
{
return texturePath.Contains("Stage/") ||
texturePath.Contains("/Body") ||
texturePath.Contains("/GrooveLight") ||
texturePath.Contains("keybase") ||
texturePath.Contains("keypress") ||
texturePath.Contains("_OverObject");
}
#endregion
#region
/// <summary>
/// 构造Note、光效等动画组件。
/// </summary>
/// <param name="component"></param>
/// <param name="isFlare">是否为光效</param>
/// <returns></returns>
public virtual TextureAnimation CreateAnimation(string component, bool? isFlare = null)
{
bool isHit = isFlare is true;
var animation = new TextureAnimation
{
Anchor = isHit ? Anchor.BottomCentre : Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = isHit ? Axes.None : Axes.Both,
FillMode = isHit ? FillMode.Fit : FillMode.Stretch,
Loop = !isHit
};
if (!isHit)
{
animation.DefaultFrameLength = default_frame_length;
// animation.Blending = BlendingParameters.Inherit;
}
if (component == "JudgementLine")
FillMode = FillMode.Fill;
var frames = getCachedTextureFrames(component);
animation.AddFrames(frames);
return animation;
}
private List<Texture> getCachedTextureFrames(string component)
{
string currentNoteSetName = noteSetName.Value;
string cacheKey = $"{currentNoteSetName}_{component}";
return getOrAddCachedFrames(cacheKey, () => loadNotesFrames(component, currentNoteSetName));
}
private List<Texture> loadNotesFrames(string component, string noteSetName)
{
var frames = new List<Texture>();
string basePath = $"note/{noteSetName}/{component}";
if (component != "JudgementLine")
{
for (int i = 0; i < max_frames_to_load; i++)
{
string frameFile = $"{basePath}/{i:D3}.png";
var texture = textureStore.Get(frameFile);
if (texture == null) break;
texture.ScaleAdjust = 2f;
frames.Add(texture);
}
}
else
{
string frameFile = $"{basePath}.png";
Logger.Log($"[EzLocalTextureFactory] Loading JudgementLine Frame: {frameFile}", Ez2ConfigManager.LOGGER_NAME, LogLevel.Debug);
var texture = textureStore.Get(frameFile);
frames.Add(texture);
}
return new List<Texture>(frames);
}
#endregion
#region Stage Creation
public virtual Container CreateStage(string component)
{
var container = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
// AutoSizeAxes = Axes.X,
// Height = default_stage_body_height,
// Masking = true,
};
// hitPositonBindable.BindValueChanged(_ =>
// {
// container.Y = ezSkinConfig.DefaultHitPosition - (float)hitPositonBindable.Value;
// }, true);
string basePath = $"Stage/{stageName.Value}/Stage";
addStageComponent(container, $"{basePath}/eightkey/{component}");
addStageComponent(container, $"{basePath}/GrooveLight"); //此纹理需要修改正片叠底
addStageComponent(container, $"{basePath}/{stageName.Value}_OverObject/{stageName.Value}_OverObject");
return container;
}
private void addStageComponent(Container container, string basePath)
{
var frames = loadStageComponentFrames(basePath);
var animation = new TextureAnimation
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Y = 384f + 247f,
// RelativeSizeAxes = Axes.None,
// FillMode = FillMode.Fill,
};
if (basePath.Contains("GrooveLight"))
animation.Blending = BlendingParameters.Additive;
animation.Loop = frames.Count > 1;
animation.Scale = frames.Count > 1
? new Vector2(2f)
: Vector2.One;
animation.AddFrames(frames);
container.Add(animation);
}
private List<Texture> loadStageComponentFrames(string basePath)
{
var frames = new List<Texture>();
for (int i = 0;; i++)
{
Texture? texture = textureStore.Get($"{basePath}_{i}.png");
if (texture == null) break;
Logger.Log($"[EzLocalTextureFactory] Added Stage Frames: {basePath}_{i}.png", Ez2ConfigManager.LOGGER_NAME, LogLevel.Debug);
frames.Add(texture);
}
if (frames.Count == 0)
{
Texture? texture = stageTextureStore.Get($"{basePath}.png");
if (texture != null)
{
Logger.Log($"[EzLocalTextureFactory] Added Stage Frame: {basePath}", Ez2ConfigManager.LOGGER_NAME, LogLevel.Debug);
frames.Add(texture);
}
}
return frames;
}
public virtual TextureAnimation CreateStageKeys(string component, string? keySuffix = null)
{
var frames = getCachedStageKeysFrames(component, keySuffix);
var animation = new TextureAnimation
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.None,
FillMode = FillMode.Stretch,
DefaultFrameLength = default_frame_length * 4,
};
animation.AddFrames(frames);
return animation;
}
private List<Texture> getCachedStageKeysFrames(string component, string? keySuffix = null)
{
string currentStageName = stageName.Value;
string cacheKey = $"{currentStageName}_{component}_{keySuffix}";
return getOrAddCachedFrames(cacheKey, () => loadStageKeysFrames(component, keySuffix));
}
private List<Texture> loadStageKeysFrames(string component, string? keySuffix = null)
{
var frames = new List<Texture>();
string currentStageName = stageName.Value;
string[] pathsToTry =
{
$"Stage/{currentStageName}/Stage/eightkey/keybase/{component}",
$"Stage/{currentStageName}/Stage/eightkey/keypress/{component}",
$"Stage/{currentStageName}/Stage/eightkey/keybase/{component}_{keySuffix}",
$"Stage/{currentStageName}/Stage/eightkey/keypress/{component}_{keySuffix}",
};
foreach (string basePath in pathsToTry)
{
for (int i = 0;; i++)
{
Texture? texture = textureStore.Get($"{basePath}_frame{i}.png");
if (texture == null) break;
Logger.Log($"[EzLocalTextureFactory] Added Keys Frames: {basePath}_{i}", Ez2ConfigManager.LOGGER_NAME, LogLevel.Debug);
frames.Add(texture);
}
// 如果没有帧,加载单个纹理作为单帧
if (frames.Count == 0)
{
Texture? texture = textureStore.Get($"{basePath}.png");
if (texture != null)
{
Logger.Log($"[EzLocalTextureFactory] Added Keys Frame: {basePath}", Ez2ConfigManager.LOGGER_NAME, LogLevel.Debug);
frames.Add(texture);
}
}
}
return frames;
}
#endregion
#region
private List<Texture> getOrAddCachedFrames(string cacheKey, Func<List<Texture>> factory)
{
// 双重检查锁定以确保线程安全,获取或添加缓存纹理
lock (cleanup_lock)
{
if (global_cache.TryGetValue(cacheKey, out var cachedEntry))
{
if (cachedEntry.Textures != null && cachedEntry.Textures.Count > 0)
{
global_cache.TryUpdate(cacheKey, cachedEntry.UpdateAccess(), cachedEntry);
return cachedEntry.Textures;
}
global_cache.TryRemove(cacheKey, out _);
}
var frames = factory();
if (frames.Count > 0)
{
var newEntry = new CacheEntry(frames, true);
global_cache.TryAdd(cacheKey, newEntry);
}
return frames;
}
}
public static void ClearGlobalCache()
{
int count1 = note_ratio_cache.Count;
if (count1 > 0)
{
Logger.Log($"[EzLocalTextureFactory] Clearing note_ratio_cache ({count1})", Ez2ConfigManager.LOGGER_NAME, LogLevel.Debug);
note_ratio_cache.Clear();
}
int count2 = global_cache.Count;
if (count2 > 0)
{
Logger.Log($"[EzLocalTextureFactory] Clearing global_cache ({count2})", Ez2ConfigManager.LOGGER_NAME, LogLevel.Debug);
global_cache.Clear();
}
}
#endregion
protected override void Dispose(bool isDisposing)
{
if (isDisposing)
{
// Unbind to avoid leaking handlers
try
{
noteSetName.UnbindAll();
stageName.UnbindAll();
columnWidth.UnbindAll();
specialFactor.UnbindAll();
hitPositonBindable.UnbindAll();
noteHeightScaleToWidth.UnbindAll();
}
catch
{
}
// 只清理实例级别的缓存,全局缓存留给 ClearGlobalCache 处理
lock (loaderStoreCache)
{
foreach (var loader in loaderStoreCache.Values)
loader.Dispose();
}
lock (loaderStoreCache)
{
loaderStoreCache.Clear();
}
note_ratio_cache.Clear();
}
base.Dispose(isDisposing);
}
}
public enum EzAnimationType
{
Note,
Hit,
Stage,
Key,
Health,
}
}