mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 11:20:04 +08:00
Implement missing behaviours required for skin file operations via RealmArchiveModelManager
This commit is contained in:
parent
e2d9a685d7
commit
29d074bdb8
@ -211,17 +211,9 @@ namespace osu.Game
|
||||
|
||||
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
||||
|
||||
dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Resources, Audio));
|
||||
dependencies.Cache(SkinManager = new SkinManager(Storage, realmFactory, Host, Resources, Audio, Scheduler));
|
||||
dependencies.CacheAs<ISkinSource>(SkinManager);
|
||||
|
||||
// needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo.
|
||||
SkinManager.ItemRemoved += item => Schedule(() =>
|
||||
{
|
||||
// check the removed skin is not the current user choice. if it is, switch back to default.
|
||||
if (item.Equals(SkinManager.CurrentSkinInfo.Value))
|
||||
SkinManager.CurrentSkinInfo.Value = SkinInfo.Default;
|
||||
});
|
||||
|
||||
EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
|
||||
|
||||
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
|
||||
|
@ -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
|
||||
|
@ -8,21 +8,26 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Stores;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class SkinModelManager : ArchiveModelManager<SkinInfo, SkinFileInfo>
|
||||
public class SkinModelManager : RealmArchiveModelManager<SkinInfo>
|
||||
{
|
||||
private readonly IStorageResourceProvider skinResources;
|
||||
|
||||
public SkinModelManager(Storage storage, DatabaseContextFactory contextFactory, SkinStore skinStore, GameHost host, IStorageResourceProvider skinResources)
|
||||
: base(storage, contextFactory, skinStore, host)
|
||||
public SkinModelManager(Storage storage, RealmContextFactory contextFactory, GameHost host, IStorageResourceProvider skinResources)
|
||||
: base(storage, contextFactory)
|
||||
{
|
||||
this.skinResources = skinResources;
|
||||
|
||||
@ -42,18 +47,27 @@ namespace osu.Game.Skinning
|
||||
|
||||
protected override bool HasCustomHashFunction => true;
|
||||
|
||||
protected override string ComputeHash(SkinInfo item)
|
||||
protected override Task Populate(SkinInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(model.InstantiationInfo))
|
||||
model.InstantiationInfo = createInstance(model).GetType().GetInvariantInstantiationInfo();
|
||||
|
||||
checkSkinIniMetadata(model, realm);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void checkSkinIniMetadata(SkinInfo item, Realm realm)
|
||||
{
|
||||
var instance = createInstance(item);
|
||||
|
||||
// This function can be run on fresh import or save. The logic here ensures a skin.ini file is in a good state for both operations.
|
||||
|
||||
// `Skin` will parse the skin.ini and populate `Skin.Configuration` during construction above.
|
||||
string skinIniSourcedName = instance.Configuration.SkinInfo.Name;
|
||||
string skinIniSourcedCreator = instance.Configuration.SkinInfo.Creator;
|
||||
string archiveName = item.Name.Replace(@".osk", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
bool isImport = item.ID == 0;
|
||||
bool isImport = !item.IsManaged;
|
||||
|
||||
if (isImport)
|
||||
{
|
||||
@ -71,12 +85,10 @@ namespace osu.Game.Skinning
|
||||
// Regardless of whether this is an import or not, let's write the skin.ini if non-existing or non-matching.
|
||||
// This is (weirdly) done inside ComputeHash to avoid adding a new method to handle this case. After switching to realm it can be moved into another place.
|
||||
if (skinIniSourcedName != item.Name)
|
||||
updateSkinIniMetadata(item);
|
||||
|
||||
return base.ComputeHash(item);
|
||||
updateSkinIniMetadata(item, realm);
|
||||
}
|
||||
|
||||
private void updateSkinIniMetadata(SkinInfo item)
|
||||
private void updateSkinIniMetadata(SkinInfo item, Realm realm)
|
||||
{
|
||||
string nameLine = @$"Name: {item.Name}";
|
||||
string authorLine = @$"Author: {item.Creator}";
|
||||
@ -95,39 +107,47 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
// In the case a skin doesn't have a skin.ini yet, let's create one.
|
||||
writeNewSkinIni();
|
||||
return;
|
||||
}
|
||||
|
||||
using (Stream stream = new MemoryStream())
|
||||
else
|
||||
{
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
using (Stream stream = new MemoryStream())
|
||||
{
|
||||
using (var existingStream = Files.Storage.GetStream(existingFile.FileInfo.GetStoragePath()))
|
||||
using (var sr = new StreamReader(existingStream))
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
{
|
||||
string line;
|
||||
while ((line = sr.ReadLine()) != null)
|
||||
using (var existingStream = Files.Storage.GetStream(existingFile.File.GetStoragePath()))
|
||||
using (var sr = new StreamReader(existingStream))
|
||||
{
|
||||
string? line;
|
||||
while ((line = sr.ReadLine()) != null)
|
||||
sw.WriteLine(line);
|
||||
}
|
||||
|
||||
sw.WriteLine();
|
||||
|
||||
foreach (string line in newLines)
|
||||
sw.WriteLine(line);
|
||||
}
|
||||
|
||||
sw.WriteLine();
|
||||
ReplaceFile(item, existingFile, stream, realm);
|
||||
|
||||
foreach (string line in newLines)
|
||||
sw.WriteLine(line);
|
||||
}
|
||||
// can be removed 20220502.
|
||||
if (!ensureIniWasUpdated(item))
|
||||
{
|
||||
Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important);
|
||||
|
||||
ReplaceFile(item, existingFile, stream);
|
||||
var existingIni = item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase));
|
||||
if (existingIni != null)
|
||||
item.Files.Remove(existingIni);
|
||||
|
||||
// can be removed 20220502.
|
||||
if (!ensureIniWasUpdated(item))
|
||||
{
|
||||
Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important);
|
||||
|
||||
DeleteFile(item, item.Files.SingleOrDefault(f => f.Filename.Equals(@"skin.ini", StringComparison.OrdinalIgnoreCase)));
|
||||
writeNewSkinIni();
|
||||
writeNewSkinIni();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The hash is already populated at this point in import.
|
||||
// As we have changed files, it needs to be recomputed.
|
||||
item.Hash = ComputeHash(item);
|
||||
|
||||
void writeNewSkinIni()
|
||||
{
|
||||
using (Stream stream = new MemoryStream())
|
||||
@ -138,8 +158,10 @@ namespace osu.Game.Skinning
|
||||
sw.WriteLine(line);
|
||||
}
|
||||
|
||||
AddFile(item, stream, @"skin.ini");
|
||||
AddFile(item, stream, @"skin.ini", realm);
|
||||
}
|
||||
|
||||
item.Hash = ComputeHash(item);
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,36 +176,52 @@ namespace osu.Game.Skinning
|
||||
return instance.Configuration.SkinInfo.Name == item.Name;
|
||||
}
|
||||
|
||||
protected override Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var instance = createInstance(model);
|
||||
|
||||
model.InstantiationInfo ??= instance.GetType().GetInvariantInstantiationInfo();
|
||||
|
||||
model.Name = instance.Configuration.SkinInfo.Name;
|
||||
model.Creator = instance.Configuration.SkinInfo.Creator;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void populateMissingHashes()
|
||||
{
|
||||
var skinsWithoutHashes = ModelStore.ConsumableItems.Where(i => i.Hash == null).ToArray();
|
||||
|
||||
foreach (SkinInfo skin in skinsWithoutHashes)
|
||||
using (var realm = ContextFactory.CreateContext())
|
||||
{
|
||||
try
|
||||
var skinsWithoutHashes = realm.All<SkinInfo>().Where(i => string.IsNullOrEmpty(i.Hash)).ToArray();
|
||||
|
||||
foreach (SkinInfo skin in skinsWithoutHashes)
|
||||
{
|
||||
Update(skin);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Delete(skin);
|
||||
Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid");
|
||||
try
|
||||
{
|
||||
Update(skin);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Delete(skin);
|
||||
Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources);
|
||||
|
||||
public void Save(Skin skin)
|
||||
{
|
||||
skin.SkinInfo.PerformWrite(s =>
|
||||
{
|
||||
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 = s.Files.FirstOrDefault(f => f.Filename == filename);
|
||||
|
||||
if (oldFile != null)
|
||||
ReplaceFile(s, oldFile, streamContent, s.Realm);
|
||||
else
|
||||
AddFile(s, streamContent, filename, s.Realm);
|
||||
}
|
||||
}
|
||||
|
||||
s.Hash = ComputeHash(s);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
172
osu.Game/Stores/RealmArchiveModelManager.cs
Normal file
172
osu.Game/Stores/RealmArchiveModelManager.cs
Normal file
@ -0,0 +1,172 @@
|
||||
// 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.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Stores
|
||||
{
|
||||
/// <summary>
|
||||
/// Class which adds all the missing pieces bridging the gap between <see cref="RealmArchiveModelImporter{TModel}"/> and <see cref="ArchiveModelManager{TModel,TFileModel}"/>.
|
||||
/// </summary>
|
||||
public abstract class RealmArchiveModelManager<TModel> : RealmArchiveModelImporter<TModel>, IModelManager<TModel>, IModelFileManager<TModel, RealmNamedFileUsage>
|
||||
where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete
|
||||
{
|
||||
public event Action<TModel>? ItemUpdated;
|
||||
public event Action<TModel>? ItemRemoved;
|
||||
|
||||
private readonly RealmFileStore realmFileStore;
|
||||
|
||||
protected RealmArchiveModelManager(Storage storage, RealmContextFactory contextFactory)
|
||||
: base(storage, contextFactory)
|
||||
{
|
||||
realmFileStore = new RealmFileStore(contextFactory, storage);
|
||||
}
|
||||
|
||||
public void DeleteFile(TModel item, RealmNamedFileUsage file) =>
|
||||
item.Realm.Write(() => DeleteFile(item, file, item.Realm));
|
||||
|
||||
public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents)
|
||||
=> item.Realm.Write(() => ReplaceFile(item, file, contents, item.Realm));
|
||||
|
||||
public void AddFile(TModel item, Stream stream, string filename)
|
||||
=> item.Realm.Write(() => AddFile(item, stream, filename, item.Realm));
|
||||
|
||||
public void DeleteFile(TModel item, RealmNamedFileUsage file, Realm realm)
|
||||
{
|
||||
item.Files.Remove(file);
|
||||
}
|
||||
|
||||
public void ReplaceFile(TModel model, RealmNamedFileUsage file, Stream contents, Realm realm)
|
||||
{
|
||||
file.File = realmFileStore.Add(contents, realm);
|
||||
}
|
||||
|
||||
public void AddFile(TModel item, Stream stream, string filename, Realm realm)
|
||||
{
|
||||
var file = realmFileStore.Add(stream, realm);
|
||||
var namedUsage = new RealmNamedFileUsage(file, filename);
|
||||
|
||||
item.Files.Add(namedUsage);
|
||||
}
|
||||
|
||||
public override async Task<ILive<TModel>?> Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var imported = await base.Import(item, archive, lowPriority, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
imported?.PerformRead(i => ItemUpdated?.Invoke(i.Detach()));
|
||||
|
||||
return imported;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete multiple items.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
public void Delete(List<TModel> items, bool silent = false)
|
||||
{
|
||||
if (items.Count == 0) return;
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
Progress = 0,
|
||||
Text = $"Preparing to delete all {HumanisedModelName}s...",
|
||||
CompletionText = $"Deleted all {HumanisedModelName}s!",
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
if (!silent)
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach (var b in items)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
notification.Text = $"Deleting {HumanisedModelName}s ({++i} of {items.Count})";
|
||||
|
||||
Delete(b);
|
||||
|
||||
notification.Progress = (float)i / items.Count;
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore multiple items that were previously deleted.
|
||||
/// This will post notifications tracking progress.
|
||||
/// </summary>
|
||||
public void Undelete(List<TModel> items, bool silent = false)
|
||||
{
|
||||
if (!items.Any()) return;
|
||||
|
||||
var notification = new ProgressNotification
|
||||
{
|
||||
CompletionText = "Restored all deleted items!",
|
||||
Progress = 0,
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
|
||||
if (!silent)
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
int i = 0;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (notification.State == ProgressNotificationState.Cancelled)
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
notification.Text = $"Restoring ({++i} of {items.Count})";
|
||||
|
||||
Undelete(item);
|
||||
|
||||
notification.Progress = (float)i / items.Count;
|
||||
}
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
}
|
||||
|
||||
public bool Delete(TModel item)
|
||||
{
|
||||
if (item.DeletePending)
|
||||
return false;
|
||||
|
||||
item.Realm.Write(r => item.DeletePending = true);
|
||||
ItemRemoved?.Invoke(item.Detach());
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Undelete(TModel item)
|
||||
{
|
||||
if (!item.DeletePending)
|
||||
return;
|
||||
|
||||
item.Realm.Write(r => item.DeletePending = false);
|
||||
ItemUpdated?.Invoke(item);
|
||||
}
|
||||
|
||||
public virtual bool IsAvailableLocally(TModel model) => false; // TODO: implement
|
||||
|
||||
public void Update(TModel skin)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user