1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-06 06:57:39 +08:00
osu-lazer/osu.Game/Skinning/SkinManager.cs

259 lines
11 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 17:19:50 +08:00
using System;
using System.Collections.Generic;
using System.IO;
2018-04-13 17:19:50 +08:00
using System.Linq;
using System.Linq.Expressions;
2021-05-10 21:43:48 +08:00
using System.Text;
using System.Threading;
using System.Threading.Tasks;
2018-04-13 17:19:50 +08:00
using Microsoft.EntityFrameworkCore;
2021-05-10 21:43:48 +08:00
using Newtonsoft.Json;
2018-04-13 17:19:50 +08:00
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables;
using osu.Framework.Extensions;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics;
2020-07-17 15:54:30 +08:00
using osu.Framework.Graphics.OpenGL.Textures;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
2018-04-13 17:19:50 +08:00
using osu.Framework.Platform;
using osu.Framework.Testing;
2020-11-11 12:05:03 +08:00
using osu.Framework.Utils;
2019-08-23 19:32:43 +08:00
using osu.Game.Audio;
2018-04-13 17:19:50 +08:00
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.IO;
2018-04-13 17:19:50 +08:00
using osu.Game.IO.Archives;
namespace osu.Game.Skinning
{
[ExcludeFromDynamicCompile]
public class SkinManager : ArchiveModelManager<SkinInfo, SkinFileInfo>, ISkinSource, IStorageResourceProvider
2018-04-13 17:19:50 +08:00
{
private readonly AudioManager audio;
private readonly GameHost host;
private readonly IResourceStore<byte[]> resources;
public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>();
2018-04-13 17:19:50 +08:00
public readonly Bindable<SkinInfo> CurrentSkinInfo = new Bindable<SkinInfo>(SkinInfo.Default) { Default = SkinInfo.Default };
2020-10-02 15:17:10 +08:00
public override IEnumerable<string> HandledExtensions => new[] { ".osk" };
2018-04-13 17:19:50 +08:00
protected override string[] HashableFileTypes => new[] { ".ini" };
protected override string ImportFromStablePath => "Skins";
private readonly Skin defaultLegacySkin;
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore<byte[]> resources, AudioManager audio)
: base(storage, contextFactory, new SkinStore(contextFactory, storage), host)
2018-11-28 18:01:22 +08:00
{
this.audio = audio;
this.host = host;
this.resources = resources;
2018-11-28 18:01:22 +08:00
defaultLegacySkin = new DefaultLegacySkin(this);
2019-07-26 18:29:06 +08:00
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
CurrentSkin.Value = new DefaultSkin(this);
2018-11-28 18:01:22 +08:00
CurrentSkin.ValueChanged += skin =>
{
if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value)
2018-11-28 18:01:22 +08:00
throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead.");
SourceChanged?.Invoke();
};
}
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osk";
2018-04-13 17:19:50 +08:00
/// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the special default skin plus all skins from <see cref="GetAllUserSkins"/>.
2018-04-13 17:19:50 +08:00
/// </summary>
2018-11-28 19:36:21 +08:00
/// <returns>A newly allocated list of available <see cref="SkinInfo"/>.</returns>
2018-04-13 17:19:50 +08:00
public List<SkinInfo> GetAllUsableSkins()
{
var userSkins = GetAllUserSkins();
2018-04-13 17:19:50 +08:00
userSkins.Insert(0, SkinInfo.Default);
userSkins.Insert(1, DefaultLegacySkin.Info);
2018-04-13 17:19:50 +08:00
return userSkins;
}
/// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s that have been loaded by the user.
/// </summary>
2018-11-28 19:36:21 +08:00
/// <returns>A newly allocated list of available <see cref="SkinInfo"/>.</returns>
public List<SkinInfo> GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
2020-11-11 12:05:03 +08:00
public void SelectRandomSkin()
{
// choose from only user skins, removing the current selection to ensure a new one is chosen.
var randomChoices = GetAllUsableSkins().Where(s => s.ID != CurrentSkinInfo.Value.ID).ToArray();
2020-11-11 12:05:03 +08:00
if (randomChoices.Length == 0)
{
CurrentSkinInfo.Value = SkinInfo.Default;
return;
}
CurrentSkinInfo.Value = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length));
}
2018-11-28 18:01:22 +08:00
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name };
2018-04-13 17:19:50 +08:00
private const string unknown_creator_string = "Unknown";
protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null)
{
// we need to populate early to create a hash based off skin.ini contents
2021-03-25 13:14:38 +08:00
if (item.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
populateMetadata(item, GetSkin(item));
if (item.Creator != null && item.Creator != unknown_creator_string)
{
// this is the optimal way to hash legacy skins, but will need to be reconsidered when we move forward with skin implementation.
// likely, the skin should expose a real version (ie. the version of the skin, not the skin.ini version it's targeting).
return item.ToString().ComputeSHA2Hash();
}
2020-09-14 19:17:00 +08:00
// if there was no creator, the ToString above would give the filename, which alone isn't really enough to base any decisions on.
return base.ComputeHash(item, reader);
}
protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
2018-04-13 17:19:50 +08:00
{
await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
2018-04-13 17:19:50 +08:00
var instance = GetSkin(model);
model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo();
2021-03-25 13:14:38 +08:00
if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
populateMetadata(model, instance);
}
private void populateMetadata(SkinInfo item, Skin instance)
{
if (!string.IsNullOrEmpty(instance.Configuration.SkinInfo.Name))
2018-04-13 17:19:50 +08:00
{
item.Name = instance.Configuration.SkinInfo.Name;
item.Creator = instance.Configuration.SkinInfo.Creator;
2018-04-13 17:19:50 +08:00
}
else
{
item.Name = item.Name.Replace(".osk", "", StringComparison.OrdinalIgnoreCase);
item.Creator ??= unknown_creator_string;
2018-04-13 17:19:50 +08: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 17:19:50 +08:00
{
if (CurrentSkinInfo.Value.ID >= 1) return;
2018-04-13 17:19:50 +08:00
var skin = CurrentSkin.Value;
// if the user is attempting to save one of the default skin implementations, create a copy first.
CurrentSkinInfo.Value = Import(new SkinInfo
{
Name = skin.SkinInfo.Name + " (modified)",
Creator = skin.SkinInfo.Creator,
InstantiationInfo = skin.SkinInfo.InstantiationInfo,
}).Result;
2018-04-13 17:19:50 +08:00
}
2021-05-10 21:43:48 +08:00
public void Save(Skin skin)
{
if (skin.SkinInfo.ID <= 0)
throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first.");
2021-05-10 21:43:48 +08:00
foreach (var drawableInfo in skin.DrawableComponentInfo)
{
string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented });
using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(json)))
{
string filename = $"{drawableInfo.Key}.json";
var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename);
if (oldFile != null)
ReplaceFile(skin.SkinInfo, oldFile, streamContent, oldFile.Filename);
else
AddFile(skin.SkinInfo, streamContent, filename);
}
}
}
2018-04-13 17:19:50 +08: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 SkinInfo Query(Expression<Func<SkinInfo, bool>> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query);
public event Action SourceChanged;
2021-06-06 11:17:55 +08:00
public Drawable GetDrawableComponent(ISkinComponent component) => lookupWithFallback(s => s.GetDrawableComponent(component));
2018-04-13 17:19:50 +08:00
2021-06-06 11:17:55 +08:00
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => lookupWithFallback(s => s.GetTexture(componentName, wrapModeS, wrapModeT));
2018-04-13 17:19:50 +08:00
2021-06-06 11:17:55 +08:00
public ISample GetSample(ISampleInfo sampleInfo) => lookupWithFallback(s => s.GetSample(sampleInfo));
2018-04-13 17:19:50 +08:00
2021-06-06 11:17:55 +08:00
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => lookupWithFallback(s => s.GetConfig<TLookup, TValue>(lookup));
public ISkin FindProvider(Func<ISkin, bool> lookupFunction)
{
if (lookupFunction(CurrentSkin.Value))
return CurrentSkin.Value;
2021-05-31 16:25:21 +08:00
if (CurrentSkin.Value is LegacySkin && lookupFunction(defaultLegacySkin))
return defaultLegacySkin;
return null;
}
2021-06-06 11:17:55 +08:00
private T lookupWithFallback<T>(Func<ISkin, T> func)
where T : class
{
var selectedSkin = func(CurrentSkin.Value);
if (selectedSkin != null)
return selectedSkin;
// TODO: we also want to return a DefaultLegacySkin here if the current *beatmap* is providing any skinned elements.
// When attempting to address this, we may want to move the full DefaultLegacySkin fallback logic to within Player itself (to better allow
// for beatmap skin visibility).
2021-06-06 11:17:55 +08:00
if (CurrentSkin.Value is LegacySkin)
return func(defaultLegacySkin);
return null;
}
2021-05-31 16:25:21 +08:00
#region IResourceStorageProvider
AudioManager IStorageResourceProvider.AudioManager => audio;
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
IResourceStore<byte[]> IStorageResourceProvider.Files => Files.Store;
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host.CreateTextureLoaderStore(underlyingStore);
#endregion
2018-04-13 17:19:50 +08:00
}
}