// Copyright (c) ppy Pty Ltd . 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.IO; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Game.Rulesets.Configuration; namespace osu.Game.Rulesets.UI { public class DrawableRulesetDependencies : DependencyContainer, IDisposable { /// /// The texture store to be used for the ruleset. /// /// /// Reads textures from the "Textures" folder in ruleset resources. /// If not available locally, lookups will fallback to the global texture store. /// public TextureStore TextureStore { get; } /// /// The sample store to be used for the ruleset. /// /// /// Reads samples from the "Samples" folder in ruleset resources. /// If not available locally, lookups will fallback to the global sample store. /// public ISampleStore SampleStore { get; } /// /// The shader manager to be used for the ruleset. /// /// /// Reads shaders from the "Shaders" folder in ruleset resources. /// If not available locally, lookups will fallback to the global shader manager. /// public ShaderManager ShaderManager { get; } /// /// The ruleset config manager. May be null if ruleset does not expose a configuration manager. /// public IRulesetConfigManager? RulesetConfigManager { get; } public DrawableRulesetDependencies(Ruleset ruleset, IReadOnlyDependencyContainer parent) : base(parent) { var resources = ruleset.CreateResourceStore(); var host = parent.Get(); TextureStore = new TextureStore(host.Renderer, parent.Get().CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); CacheAs(TextureStore = new FallbackTextureStore(host.Renderer, TextureStore, parent.Get())); SampleStore = parent.Get().GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get())); CacheAs(ShaderManager = new RulesetShaderManager(host.Renderer, new NamespacedResourceStore(resources, @"Shaders"), parent.Get())); RulesetConfigManager = parent.Get().GetConfigFor(ruleset); if (RulesetConfigManager != null) Cache(RulesetConfigManager); } #region Disposal ~DrawableRulesetDependencies() { // required to potentially clean up sample store from audio hierarchy. Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private bool isDisposed; protected void Dispose(bool disposing) { if (isDisposed) return; isDisposed = true; if (SampleStore.IsNotNull()) SampleStore.Dispose(); if (TextureStore.IsNotNull()) TextureStore.Dispose(); if (ShaderManager.IsNotNull()) ShaderManager.Dispose(); } #endregion /// /// A sample store which adds a fallback source and prevents disposal of the fallback source. /// private class FallbackSampleStore : ISampleStore { private readonly ISampleStore primary; private readonly ISampleStore fallback; public FallbackSampleStore(ISampleStore primary, ISampleStore fallback) { this.primary = primary; this.fallback = fallback; } public Sample Get(string name) => primary.Get(name) ?? fallback.Get(name); public async Task GetAsync(string name, CancellationToken cancellationToken = default) { return await primary.GetAsync(name, cancellationToken).ConfigureAwait(false) ?? await fallback.GetAsync(name, cancellationToken).ConfigureAwait(false); } public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.GetStream(name); public IEnumerable GetAvailableResources() => throw new NotSupportedException(); public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => throw new NotSupportedException(); public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => throw new NotSupportedException(); public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException(); public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); public BindableNumber Volume => throw new NotSupportedException(); public BindableNumber Balance => throw new NotSupportedException(); public BindableNumber Frequency => throw new NotSupportedException(); public BindableNumber Tempo => throw new NotSupportedException(); public IBindable AggregateVolume => throw new NotSupportedException(); public IBindable AggregateBalance => throw new NotSupportedException(); public IBindable AggregateFrequency => throw new NotSupportedException(); public IBindable AggregateTempo => throw new NotSupportedException(); public int PlaybackConcurrency { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public void AddExtension(string extension) => throw new NotSupportedException(); public void Dispose() { if (primary.IsNotNull()) primary.Dispose(); } } /// /// A texture store which adds a fallback source and prevents disposal of the fallback source. /// private class FallbackTextureStore : TextureStore { private readonly TextureStore primary; private readonly TextureStore fallback; public FallbackTextureStore(IRenderer renderer, TextureStore primary, TextureStore fallback) : base(renderer) { this.primary = primary; this.fallback = fallback; } public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => primary.Get(name, wrapModeS, wrapModeT) ?? fallback.Get(name, wrapModeS, wrapModeT); protected override void Dispose(bool disposing) { base.Dispose(disposing); if (primary.IsNotNull()) primary.Dispose(); } } private class RulesetShaderManager : ShaderManager { private readonly ShaderManager parent; public RulesetShaderManager(IRenderer renderer, NamespacedResourceStore rulesetResources, ShaderManager parent) : base(renderer, rulesetResources) { this.parent = parent; } // When the debugger is attached, exceptions are expensive. // Manually work around this by caching failed lookups and falling back straight to parent. private readonly HashSet<(string, string)> failedLookups = new HashSet<(string, string)>(); public override IShader Load(string vertex, string fragment) { if (!failedLookups.Contains((vertex, fragment))) { try { return base.Load(vertex, fragment); } catch { // Shader lookup is very non-standard. Rather than returning null on missing shaders, exceptions are thrown. failedLookups.Add((vertex, fragment)); } } return parent.Load(vertex, fragment); } } } }