osu/osu.Game/Skinning/SkinManager.cs

310 lines
12 KiB
C#
Raw Normal View History

// 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.
2018-04-13 09:19:50 +00:00
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
2018-04-13 09:19:50 +00:00
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
2019-02-21 10:04:31 +00:00
using osu.Framework.Bindables;
2018-04-13 09:19:50 +00:00
using osu.Framework.Graphics;
2020-07-17 07:54:30 +00:00
using osu.Framework.Graphics.OpenGL.Textures;
2018-04-13 09:19:50 +00:00
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
2018-04-13 09:19:50 +00:00
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Threading;
2020-11-11 04:05:03 +00:00
using osu.Framework.Utils;
2019-08-23 11:32:43 +00:00
using osu.Game.Audio;
2018-04-13 09:19:50 +00:00
using osu.Game.Database;
using osu.Game.IO;
2018-04-13 09:19:50 +00:00
using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
2018-04-13 09:19:50 +00:00
namespace osu.Game.Skinning
{
/// <summary>
/// Handles the storage and retrieval of <see cref="Skin"/>s.
/// </summary>
/// <remarks>
/// This is also exposed and cached as <see cref="ISkinSource"/> to allow for any component to potentially have skinning support.
/// For gameplay components, see <see cref="RulesetSkinProvidingContainer"/> which adds extra legacy and toggle logic that may affect the lookup process.
/// </remarks>
[ExcludeFromDynamicCompile]
public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo>
2018-04-13 09:19:50 +00:00
{
private readonly AudioManager audio;
private readonly GameHost host;
private readonly IResourceStore<byte[]> resources;
public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>();
2021-11-29 08:57:17 +00:00
public readonly Bindable<ILive<SkinInfo>> CurrentSkinInfo = new Bindable<ILive<SkinInfo>>(SkinInfo.Default.ToLive()) { Default = SkinInfo.Default.ToLive() };
2018-04-13 09:19:50 +00:00
private readonly SkinModelManager skinModelManager;
private readonly RealmContextFactory contextFactory;
private readonly IResourceStore<byte[]> userFiles;
/// <summary>
2021-06-11 05:50:21 +00:00
/// The default skin.
/// </summary>
public Skin DefaultSkin { get; }
/// <summary>
2021-06-11 05:50:21 +00:00
/// The default legacy skin.
/// </summary>
public Skin DefaultLegacySkin { get; }
public SkinManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IResourceStore<byte[]> resources, AudioManager audio, Scheduler scheduler)
2018-11-28 10:01:22 +00:00
{
this.contextFactory = contextFactory;
2018-11-28 10:01:22 +00:00
this.audio = audio;
this.host = host;
this.resources = resources;
2018-11-28 10:01:22 +00:00
userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files"));
skinModelManager = new SkinModelManager(storage, contextFactory, host, this);
DefaultLegacySkin = new DefaultLegacySkin(this);
DefaultSkin = new DefaultSkin(this);
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
CurrentSkin.Value = DefaultSkin;
2018-11-28 10:01:22 +00:00
CurrentSkin.ValueChanged += skin =>
{
if (!skin.NewValue.SkinInfo.Equals(CurrentSkinInfo.Value))
2018-11-28 10:01:22 +00:00
throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead.");
SourceChanged?.Invoke();
};
// needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo.
ItemRemoved += item => scheduler.Add(() =>
{
// TODO: fix.
// check the removed skin is not the current user choice. if it is, switch back to default.
// if (item.Equals(CurrentSkinInfo.Value))
// CurrentSkinInfo.Value = SkinInfo.Default;
});
2018-04-13 09:19:50 +00:00
}
/// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the non-databased default skins.
/// </summary>
2018-11-28 11:36:21 +00:00
/// <returns>A newly allocated list of available <see cref="SkinInfo"/>.</returns>
public List<ILive<SkinInfo>> GetAllUsableSkins()
{
using (var context = contextFactory.CreateContext())
{
var userSkins = context.All<SkinInfo>().Where(s => !s.DeletePending).ToLive();
userSkins.Insert(0, DefaultSkin.SkinInfo);
userSkins.Insert(1, DefaultLegacySkin.SkinInfo);
return userSkins;
}
}
2020-11-11 04:05:03 +00:00
public void SelectRandomSkin()
{
using (var context = contextFactory.CreateContext())
2020-11-11 04:05:03 +00:00
{
// choose from only user skins, removing the current selection to ensure a new one is chosen.
var randomChoices = context.All<SkinInfo>().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray();
2020-11-11 04:05:03 +00:00
if (randomChoices.Length == 0)
{
CurrentSkinInfo.Value = SkinInfo.Default.ToLive();
return;
}
var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length));
CurrentSkinInfo.Value = chosen.ToLive();
}
2018-04-13 09:19:50 +00:00
}
/// <summary>
/// Retrieve a <see cref="Skin"/> instance for the provided <see cref="SkinInfo"/>
/// </summary>
/// <param name="skinInfo">The skin to lookup.</param>
/// <returns>A <see cref="Skin"/> instance correlating to the provided <see cref="SkinInfo"/>.</returns>
public Skin GetSkin(SkinInfo skinInfo) => skinInfo.CreateInstance(this);
/// <summary>
/// Ensure that the current skin is in a state it can accept user modifications.
/// This will create a copy of any internal skin and being tracking in the database if not already.
/// </summary>
public void EnsureMutableSkin()
2018-04-13 09:19:50 +00:00
{
CurrentSkinInfo.Value.PerformRead(s =>
{
if (s.IsManaged)
return;
// if the user is attempting to save one of the default skin implementations, create a copy first.
var result = skinModelManager.Import(new SkinInfo
{
Name = s.Name + @" (modified)",
Creator = s.Creator,
InstantiationInfo = s.InstantiationInfo,
}).Result;
if (result != null)
CurrentSkinInfo.Value = result;
});
2018-04-13 09:19:50 +00:00
}
2021-05-10 13:43:48 +00:00
public void Save(Skin skin)
{
if (!skin.SkinInfo.IsManaged)
throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first.");
2021-05-10 13:43:48 +00:00
skinModelManager.Save(skin);
2021-05-10 13:43:48 +00:00
}
2018-04-13 09:19:50 +00:00
/// <summary>
/// Perform a lookup query on available <see cref="SkinInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public ILive<SkinInfo> Query(Expression<Func<SkinInfo, bool>> query)
{
using (var context = contextFactory.CreateContext())
return context.All<SkinInfo>().FirstOrDefault(query)?.ToLive();
}
2018-04-13 09:19:50 +00:00
public event Action SourceChanged;
2021-06-06 03:17:55 +00:00
public Drawable GetDrawableComponent(ISkinComponent component) => lookupWithFallback(s => s.GetDrawableComponent(component));
2018-04-13 09:19:50 +00:00
2021-06-06 03:17:55 +00:00
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => lookupWithFallback(s => s.GetTexture(componentName, wrapModeS, wrapModeT));
2018-04-13 09:19:50 +00:00
2021-06-06 03:17:55 +00:00
public ISample GetSample(ISampleInfo sampleInfo) => lookupWithFallback(s => s.GetSample(sampleInfo));
2018-04-13 09:19:50 +00:00
2021-06-06 03:17:55 +00:00
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => lookupWithFallback(s => s.GetConfig<TLookup, TValue>(lookup));
public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
{
foreach (var source in AllSources)
{
if (lookupFunction(source))
return source;
}
return null;
}
2021-06-06 03:17:55 +00:00
public IEnumerable<ISkin> AllSources
{
get
{
yield return CurrentSkin.Value;
if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultLegacySkin)
yield return DefaultLegacySkin;
if (CurrentSkin.Value != DefaultSkin)
yield return DefaultSkin;
}
}
private T lookupWithFallback<T>(Func<ISkin, T> lookupFunction)
2021-06-06 03:17:55 +00:00
where T : class
{
foreach (var source in AllSources)
{
if (lookupFunction(source) is T skinSourced)
return skinSourced;
}
2021-06-06 03:17:55 +00:00
return null;
2021-06-06 03:17:55 +00:00
}
2021-05-31 08:25:21 +00:00
#region IResourceStorageProvider
AudioManager IStorageResourceProvider.AudioManager => audio;
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
IResourceStore<byte[]> IStorageResourceProvider.Files => userFiles;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
#endregion
#region Implementation of IModelImporter<SkinInfo>
public Action<Notification> PostNotification
{
set => skinModelManager.PostNotification = value;
}
public Action<IEnumerable<ILive<SkinInfo>>> PostImport
{
set => skinModelManager.PostImport = value;
}
public Task Import(params string[] paths)
{
return skinModelManager.Import(paths);
}
public Task Import(params ImportTask[] tasks)
{
return skinModelManager.Import(tasks);
}
public IEnumerable<string> HandledExtensions => skinModelManager.HandledExtensions;
public Task<IEnumerable<ILive<SkinInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks)
{
return skinModelManager.Import(notification, tasks);
}
public Task<ILive<SkinInfo>> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
{
return skinModelManager.Import(task, lowPriority, cancellationToken);
}
public Task<ILive<SkinInfo>> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
{
return skinModelManager.Import(archive, lowPriority, cancellationToken);
}
public Task<ILive<SkinInfo>> Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
{
return skinModelManager.Import(item, archive, lowPriority, cancellationToken);
}
#endregion
#region Implementation of IModelManager<SkinInfo>
public event Action<SkinInfo> ItemUpdated
{
add => skinModelManager.ItemUpdated += value;
remove => skinModelManager.ItemUpdated -= value;
}
public event Action<SkinInfo> ItemRemoved
{
add => skinModelManager.ItemRemoved += value;
remove => skinModelManager.ItemRemoved -= value;
}
public void Delete(Expression<Func<SkinInfo, bool>> filter, bool silent = false)
{
using (var context = contextFactory.CreateContext())
{
var items = context.All<SkinInfo>().Where(filter).ToList();
skinModelManager.Delete(items, silent);
}
}
#endregion
2018-04-13 09:19:50 +00:00
}
}