Implement missing behaviours required for skin file operations via RealmArchiveModelManager

This commit is contained in:
Dean Herbert
2021-11-29 18:07:32 +09:00
parent e2d9a685d7
commit 29d074bdb8
4 changed files with 332 additions and 157 deletions

View File

@ -3,14 +3,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
@ -20,6 +16,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Database;
@ -37,7 +34,7 @@ namespace osu.Game.Skinning
/// 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>, IModelManager<SkinInfo>
public class SkinManager : ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo>
{
private readonly AudioManager audio;
@ -49,8 +46,7 @@ namespace osu.Game.Skinning
public readonly Bindable<ILive<SkinInfo>> CurrentSkinInfo = new Bindable<ILive<SkinInfo>>(SkinInfo.Default.ToLive()) { Default = SkinInfo.Default.ToLive() };
private readonly SkinModelManager skinModelManager;
private readonly SkinStore skinStore;
private readonly RealmContextFactory contextFactory;
private readonly IResourceStore<byte[]> userFiles;
@ -64,69 +60,73 @@ namespace osu.Game.Skinning
/// </summary>
public Skin DefaultLegacySkin { get; }
public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, IResourceStore<byte[]> resources, AudioManager audio)
public SkinManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IResourceStore<byte[]> resources, AudioManager audio, Scheduler scheduler)
{
this.contextFactory = contextFactory;
this.audio = audio;
this.host = host;
this.resources = resources;
skinStore = new SkinStore(contextFactory, storage);
userFiles = new FileStore(contextFactory, storage).Store;
userFiles = new StorageBackedResourceStore(storage.GetStorageForDirectory("files"));
skinModelManager = new SkinModelManager(storage, contextFactory, skinStore, host, this);
skinModelManager = new SkinModelManager(storage, contextFactory, host, this);
DefaultLegacySkin = new DefaultLegacySkin(this);
DefaultSkin = new DefaultSkin(this);
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
CurrentSkin.Value = DefaultSkin;
CurrentSkin.ValueChanged += skin =>
{
if (skin.NewValue.SkinInfo != CurrentSkinInfo.Value)
if (!skin.NewValue.SkinInfo.Equals(CurrentSkinInfo.Value))
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;
});
}
/// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the special default skin plus all skins from <see cref="GetAllUserSkins"/>.
/// Returns a list of all usable <see cref="SkinInfo"/>s. Includes the non-databased default skins.
/// </summary>
/// <returns>A newly allocated list of available <see cref="SkinInfo"/>.</returns>
public List<SkinInfo> GetAllUsableSkins()
public List<ILive<SkinInfo>> GetAllUsableSkins()
{
var userSkins = GetAllUserSkins();
userSkins.Insert(0, DefaultSkin.SkinInfo);
userSkins.Insert(1, DefaultLegacySkin.SkinInfo);
return userSkins;
}
/// <summary>
/// Returns a list of all usable <see cref="SkinInfo"/>s that have been loaded by the user.
/// </summary>
/// <returns>A newly allocated list of available <see cref="SkinInfo"/>.</returns>
public List<SkinInfo> GetAllUserSkins(bool includeFiles = false)
{
if (includeFiles)
return skinStore.ConsumableItems.Where(s => !s.DeletePending).ToList();
return skinStore.Items.Where(s => !s.DeletePending).ToList();
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;
}
}
public void SelectRandomSkin()
{
// choose from only user skins, removing the current selection to ensure a new one is chosen.
var randomChoices = skinStore.Items.Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray();
if (randomChoices.Length == 0)
using (var context = contextFactory.CreateContext())
{
CurrentSkinInfo.Value = SkinInfo.Default;
return;
}
// 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();
var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length));
CurrentSkinInfo.Value = skinStore.ConsumableItems.Single(i => i.ID == chosen.ID);
if (randomChoices.Length == 0)
{
CurrentSkinInfo.Value = SkinInfo.Default.ToLive();
return;
}
var chosen = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length));
CurrentSkinInfo.Value = chosen.ToLive();
}
}
/// <summary>
@ -142,40 +142,30 @@ namespace osu.Game.Skinning
/// </summary>
public void EnsureMutableSkin()
{
if (CurrentSkinInfo.Value.ID >= 1) return;
var skin = CurrentSkin.Value;
// if the user is attempting to save one of the default skin implementations, create a copy first.
CurrentSkinInfo.Value = skinModelManager.Import(new SkinInfo
CurrentSkinInfo.Value.PerformRead(s =>
{
Name = skin.SkinInfo.Name + @" (modified)",
Creator = skin.SkinInfo.Creator,
InstantiationInfo = skin.SkinInfo.InstantiationInfo,
}).Result.Value;
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;
});
}
public void Save(Skin skin)
{
if (skin.SkinInfo.ID <= 0)
if (!skin.SkinInfo.IsManaged)
throw new InvalidOperationException($"Attempting to save a skin which is not yet tracked. Call {nameof(EnsureMutableSkin)} first.");
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)
skinModelManager.ReplaceFile(skin.SkinInfo, oldFile, streamContent);
else
skinModelManager.AddFile(skin.SkinInfo, streamContent, filename);
}
}
skinModelManager.Save(skin);
}
/// <summary>
@ -183,7 +173,11 @@ namespace osu.Game.Skinning
/// </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) => skinStore.ConsumableItems.AsNoTracking().FirstOrDefault(query);
public ILive<SkinInfo> Query(Expression<Func<SkinInfo, bool>> query)
{
using (var context = contextFactory.CreateContext())
return context.All<SkinInfo>().FirstOrDefault(query)?.ToLive();
}
public event Action SourceChanged;
@ -301,34 +295,13 @@ namespace osu.Game.Skinning
remove => skinModelManager.ItemRemoved -= value;
}
public void Update(SkinInfo item)
public void Delete(Expression<Func<SkinInfo, bool>> filter, bool silent = false)
{
skinModelManager.Update(item);
}
public bool Delete(SkinInfo item)
{
return skinModelManager.Delete(item);
}
public void Delete(List<SkinInfo> items, bool silent = false)
{
skinModelManager.Delete(items, silent);
}
public void Undelete(List<SkinInfo> items, bool silent = false)
{
skinModelManager.Undelete(items, silent);
}
public void Undelete(SkinInfo item)
{
skinModelManager.Undelete(item);
}
public bool IsAvailableLocally(SkinInfo model)
{
return skinModelManager.IsAvailableLocally(model);
using (var context = contextFactory.CreateContext())
{
var items = context.All<SkinInfo>().Where(filter).ToList();
skinModelManager.Delete(items, silent);
}
}
#endregion