mirror of
https://github.com/ppy/osu.git
synced 2026-05-19 03:11:23 +08:00
Add way to add/remove custom beatmap samples to setup screen
Among others, this features a scary-looking change wherein `WorkingBeatmap` now exposes a `RealmAccess` via `IStorageResourceProvider`. This is necessary to make `RealmBackedResourceStore` actually start firing callbacks when the edited beatmap's skin is modified by adding new samples to it.
This commit is contained in:
@@ -95,7 +95,7 @@ namespace osu.Game.Beatmaps
|
||||
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap? defaultBeatmap,
|
||||
GameHost? host)
|
||||
{
|
||||
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
|
||||
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host, Realm);
|
||||
}
|
||||
|
||||
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm);
|
||||
|
||||
@@ -47,12 +47,13 @@ namespace osu.Game.Beatmaps
|
||||
private readonly LargeTextureStore beatmapPanelTextureStore;
|
||||
private readonly ITrackStore trackStore;
|
||||
private readonly IResourceStore<byte[]> files;
|
||||
private readonly RealmAccess realm;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly GameHost host;
|
||||
|
||||
public WorkingBeatmapCache(ITrackStore trackStore, AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> files, WorkingBeatmap defaultBeatmap = null,
|
||||
GameHost host = null)
|
||||
GameHost host = null, RealmAccess realm = null)
|
||||
{
|
||||
DefaultBeatmap = defaultBeatmap;
|
||||
|
||||
@@ -63,6 +64,7 @@ namespace osu.Game.Beatmaps
|
||||
largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files));
|
||||
beatmapPanelTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), new BeatmapPanelBackgroundTextureLoaderStore(host?.CreateTextureLoaderStore(files)));
|
||||
this.trackStore = trackStore;
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
public void Invalidate(BeatmapSetInfo info)
|
||||
@@ -118,7 +120,7 @@ namespace osu.Game.Beatmaps
|
||||
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
|
||||
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
|
||||
AudioManager IStorageResourceProvider.AudioManager => audioManager;
|
||||
RealmAccess IStorageResourceProvider.RealmAccess => null!;
|
||||
RealmAccess IStorageResourceProvider.RealmAccess => realm;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Files => files;
|
||||
IResourceStore<byte[]> IStorageResourceProvider.Resources => resources;
|
||||
IResourceStore<TextureUpload> IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore);
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Components
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
public Func<FileInfo, string>? SampleAddRequested { get; init; }
|
||||
public Func<FileInfo, string, string>? SampleAddRequested { get; init; }
|
||||
public Action<string>? SampleRemoveRequested { get; init; }
|
||||
|
||||
private readonly BindableWithCurrent<EditorBeatmapSkin.SampleSet?> current = new BindableWithCurrent<EditorBeatmapSkin.SampleSet?>();
|
||||
@@ -194,7 +194,7 @@ namespace osu.Game.Screens.Edit.Components
|
||||
/// <summary>
|
||||
/// Invoked when a new sample is selected via this button.
|
||||
/// </summary>
|
||||
public Func<FileInfo, string>? SampleAddRequested { get; init; }
|
||||
public Func<FileInfo, string, string>? SampleAddRequested { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a sample removal is selected via this button.
|
||||
@@ -294,7 +294,18 @@ namespace osu.Game.Screens.Edit.Components
|
||||
|
||||
AddInternal(hoverSounds = (ActualFilename.Value == null ? new HoverClickSounds(HoverSampleSet.Button) : new HoverSounds(HoverSampleSet.Button)));
|
||||
|
||||
sample = ActualFilename.Value == null ? null : editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value);
|
||||
if (ActualFilename.Value != null)
|
||||
{
|
||||
// to cover all bases, invalidate the extensionless filename (which gameplay is most likely to use)
|
||||
// as well as the filename with extension (which we are using here).
|
||||
editorBeatmap?.BeatmapSkin?.Skin.Samples?.Invalidate(ExpectedFilename.Value);
|
||||
editorBeatmap?.BeatmapSkin?.Skin.Samples?.Invalidate(ActualFilename.Value);
|
||||
sample = editorBeatmap?.BeatmapSkin?.Skin.Samples?.Get(ActualFilename.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
sample = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
@@ -317,7 +328,7 @@ namespace osu.Game.Screens.Edit.Components
|
||||
return;
|
||||
|
||||
this.HidePopover();
|
||||
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value) ?? selectedFile.Value.ToString();
|
||||
ActualFilename.Value = SampleAddRequested?.Invoke(selectedFile.Value, ExpectedFilename.Value) ?? selectedFile.Value.ToString();
|
||||
}
|
||||
|
||||
private void deleteSample()
|
||||
|
||||
@@ -101,9 +101,9 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo;
|
||||
|
||||
if (beatmapSkin is Skin skin)
|
||||
if (beatmapSkin is LegacyBeatmapSkin skin)
|
||||
{
|
||||
BeatmapSkin = new EditorBeatmapSkin(skin);
|
||||
BeatmapSkin = new EditorBeatmapSkin(playableBeatmap.BeatmapInfo!.BeatmapSet!, skin);
|
||||
BeatmapSkin.BeatmapSkinChanged += SaveState;
|
||||
}
|
||||
|
||||
@@ -532,5 +532,11 @@ namespace osu.Game.Screens.Edit
|
||||
public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor;
|
||||
|
||||
public int BeatDivisor => beatDivisor?.Value ?? 1;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
BeatmapSkin?.Dispose();
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@@ -18,14 +19,14 @@ namespace osu.Game.Screens.Edit
|
||||
/// <summary>
|
||||
/// A beatmap skin which is being edited.
|
||||
/// </summary>
|
||||
public class EditorBeatmapSkin : ISkin
|
||||
public class EditorBeatmapSkin : ISkin, IDisposable
|
||||
{
|
||||
public event Action? BeatmapSkinChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The underlying beatmap skin.
|
||||
/// </summary>
|
||||
protected internal readonly Skin Skin;
|
||||
protected internal readonly LegacyBeatmapSkin Skin;
|
||||
|
||||
/// <summary>
|
||||
/// The combo colours of this skin.
|
||||
@@ -33,7 +34,7 @@ namespace osu.Game.Screens.Edit
|
||||
/// </summary>
|
||||
public BindableList<Colour4> ComboColours { get; }
|
||||
|
||||
public EditorBeatmapSkin(Skin skin)
|
||||
public EditorBeatmapSkin(BeatmapSetInfo beatmapSet, LegacyBeatmapSkin skin)
|
||||
{
|
||||
Skin = skin;
|
||||
|
||||
@@ -50,9 +51,14 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
|
||||
ComboColours.BindCollectionChanged((_, _) => updateColours());
|
||||
|
||||
if (skin.BeatmapSetResources != null)
|
||||
skin.BeatmapSetResources.CacheInvalidated += InvokeSkinChanged;
|
||||
}
|
||||
|
||||
private void invokeSkinChanged() => BeatmapSkinChanged?.Invoke();
|
||||
public void InvokeSkinChanged() => BeatmapSkinChanged?.Invoke();
|
||||
|
||||
#region Combo colours
|
||||
|
||||
private void updateColours()
|
||||
{
|
||||
@@ -60,9 +66,13 @@ namespace osu.Game.Screens.Edit
|
||||
Skin.Configuration.CustomComboColours.Clear();
|
||||
for (int i = 0; i < ComboColours.Count; ++i)
|
||||
Skin.Configuration.CustomComboColours.Add(ComboColours[(ComboColours.Count + i - 1) % ComboColours.Count]);
|
||||
invokeSkinChanged();
|
||||
InvokeSkinChanged();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sample sets
|
||||
|
||||
public record SampleSet(int SampleSetIndex, string Name)
|
||||
{
|
||||
public SampleSet(int sampleSetIndex)
|
||||
@@ -88,7 +98,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
string[] possiblePrefixes = possibleSounds.SelectMany(sound => possibleBanks.Select(bank => $@"{bank}-{sound}")).ToArray();
|
||||
|
||||
HashSet<int> indices = new HashSet<int>();
|
||||
Dictionary<int, SampleSet> sampleSets = new Dictionary<int, SampleSet>();
|
||||
|
||||
if (Skin.Samples != null)
|
||||
{
|
||||
@@ -96,19 +106,38 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
foreach (string possiblePrefix in possiblePrefixes)
|
||||
{
|
||||
if (!sample.StartsWith(possiblePrefix, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (!sample.StartsWith(possiblePrefix, StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
string indexString = Path.GetFileNameWithoutExtension(sample)[possiblePrefix.Length..];
|
||||
int? index = null;
|
||||
|
||||
if (string.IsNullOrEmpty(indexString))
|
||||
indices.Add(1);
|
||||
if (int.TryParse(indexString, out int index))
|
||||
indices.Add(index);
|
||||
index = 1;
|
||||
if (int.TryParse(indexString, out int parsed))
|
||||
index = parsed;
|
||||
|
||||
if (!index.HasValue)
|
||||
continue;
|
||||
|
||||
SampleSet? sampleSet;
|
||||
if (!sampleSets.TryGetValue(index.Value, out sampleSet))
|
||||
sampleSet = sampleSets[index.Value] = new SampleSet(index.Value);
|
||||
|
||||
sampleSet.Filenames.Add(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return indices.OrderBy(i => i).Select(i => new SampleSet(i));
|
||||
return sampleSets.OrderBy(i => i.Key).Select(i => i.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Skin.BeatmapSetResources != null)
|
||||
Skin.BeatmapSetResources.CacheInvalidated -= InvokeSkinChanged;
|
||||
}
|
||||
|
||||
#region Delegated ISkin implementation
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
public partial class FormSampleSetChooser : FormDropdown<EditorBeatmapSkin.SampleSet?>, IHasPopover
|
||||
{
|
||||
private EditorBeatmapSkin? beatmapSkin;
|
||||
|
||||
public FormSampleSetChooser()
|
||||
{
|
||||
Caption = "Custom sample sets";
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
beatmapSkin = editorBeatmap.BeatmapSkin;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
populateItems();
|
||||
if (beatmapSkin != null)
|
||||
beatmapSkin.BeatmapSkinChanged += populateItems;
|
||||
|
||||
Current.Value = Items.FirstOrDefault(i => i?.SampleSetIndex > 0);
|
||||
Current.BindValueChanged(val =>
|
||||
{
|
||||
if (val.NewValue?.SampleSetIndex == -1)
|
||||
this.ShowPopover();
|
||||
});
|
||||
}
|
||||
|
||||
private void populateItems()
|
||||
{
|
||||
var items = beatmapSkin?.GetAvailableSampleSets().ToList() ?? [];
|
||||
items.Add(new EditorBeatmapSkin.SampleSet(-1, "Add new..."));
|
||||
Items = items;
|
||||
}
|
||||
|
||||
protected override LocalisableString GenerateItemText(EditorBeatmapSkin.SampleSet? item)
|
||||
{
|
||||
if (item == null)
|
||||
return string.Empty;
|
||||
|
||||
return base.GenerateItemText(item);
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new NewSampleSetPopover(
|
||||
Items.Any(i => i?.SampleSetIndex > 0) ? Items.Max(i => i!.SampleSetIndex) : 0,
|
||||
idx =>
|
||||
{
|
||||
if (idx == null)
|
||||
{
|
||||
Current.Value = Items.FirstOrDefault(i => i?.SampleSetIndex > 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Items.SingleOrDefault(i => i?.SampleSetIndex == idx) is EditorBeatmapSkin.SampleSet existing)
|
||||
{
|
||||
Current.Value = existing;
|
||||
return;
|
||||
}
|
||||
|
||||
var sampleSet = new EditorBeatmapSkin.SampleSet(idx.Value, $@"Custom #{idx}");
|
||||
var newItems = Items.ToList();
|
||||
newItems.Insert(newItems.Count - 1, sampleSet);
|
||||
Items = newItems;
|
||||
Current.Value = sampleSet;
|
||||
});
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (beatmapSkin != null)
|
||||
beatmapSkin.BeatmapSkinChanged -= populateItems;
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
private partial class NewSampleSetPopover : OsuPopover
|
||||
{
|
||||
private readonly int currentLargestIndex;
|
||||
private readonly Action<int?> onCommit;
|
||||
|
||||
private int? committedIndex;
|
||||
|
||||
private LabelledNumberBox numberBox = null!;
|
||||
|
||||
public NewSampleSetPopover(int currentLargestIndex, Action<int?> onCommit)
|
||||
{
|
||||
this.currentLargestIndex = currentLargestIndex;
|
||||
this.onCommit = onCommit;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = numberBox = new LabelledNumberBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.None,
|
||||
Width = 250,
|
||||
Label = "Sample set index",
|
||||
Current = { Value = (currentLargestIndex + 1).ToString(CultureInfo.InvariantCulture) }
|
||||
};
|
||||
numberBox.OnCommit += (_, _) =>
|
||||
{
|
||||
committedIndex = int.Parse(numberBox.Current.Value);
|
||||
Hide();
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
base.OnFocus(e);
|
||||
// avoids infinite refocus loop
|
||||
if (committedIndex == null)
|
||||
GetContainingFocusManager()?.ChangeFocus(numberBox);
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
if (State.Value == Visibility.Visible)
|
||||
onCommit.Invoke(committedIndex > 0 ? committedIndex : null);
|
||||
base.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using osu.Game.Localisation;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
@@ -23,6 +24,8 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
private FormBeatmapFileSelector audioTrackChooser = null!;
|
||||
private FormBeatmapFileSelector backgroundChooser = null!;
|
||||
|
||||
private readonly Bindable<EditorBeatmapSkin.SampleSet?> currentSampleSet = new Bindable<EditorBeatmapSkin.SampleSet?>();
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.ResourcesHeader;
|
||||
|
||||
[Resolved]
|
||||
@@ -65,6 +68,27 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Caption = EditorSetupStrings.AudioTrack,
|
||||
PlaceholderText = EditorSetupStrings.ClickToSelectTrack,
|
||||
},
|
||||
new FormSampleSetChooser
|
||||
{
|
||||
Current = { BindTarget = currentSampleSet },
|
||||
},
|
||||
new FormSampleSet
|
||||
{
|
||||
Current = { BindTarget = currentSampleSet },
|
||||
SampleAddRequested = (file, targetName) =>
|
||||
{
|
||||
string actualFilename = string.Concat(targetName, file.Extension);
|
||||
using var stream = file.OpenRead();
|
||||
beatmaps.AddFile(working.Value.BeatmapSetInfo, stream, actualFilename);
|
||||
return actualFilename;
|
||||
},
|
||||
SampleRemoveRequested = filename =>
|
||||
{
|
||||
var file = working.Value.BeatmapSetInfo.GetFile(filename);
|
||||
if (file != null)
|
||||
beatmaps.DeleteFile(working.Value.BeatmapSetInfo, file);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
backgroundChooser.PreviewContainer.Add(headerBackground);
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace osu.Game.Skinning
|
||||
// 2. https://github.com/peppy/osu-stable-reference/blob/dc0994645801010d4b628fff5ff79cd3c286ca83/osu!/Graphics/Textures/TextureManager.cs#L158-L196 (user skin textures lookup)
|
||||
protected override bool AllowHighResolutionSprites => false;
|
||||
|
||||
public RealmBackedResourceStore<BeatmapSetInfo>? BeatmapSetResources => FallbackStore as RealmBackedResourceStore<BeatmapSetInfo>;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new legacy beatmap skin instance.
|
||||
/// </summary>
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace osu.Game.Skinning
|
||||
public class RealmBackedResourceStore<T> : ResourceStore<byte[]>
|
||||
where T : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey
|
||||
{
|
||||
public event Action? CacheInvalidated;
|
||||
|
||||
private Lazy<Dictionary<string, string>> fileToStoragePathMapping;
|
||||
|
||||
private readonly Live<T> liveSource;
|
||||
@@ -56,7 +58,11 @@ namespace osu.Game.Skinning
|
||||
private string? getPathForFile(string filename) =>
|
||||
fileToStoragePathMapping.Value.GetValueOrDefault(filename.ToLowerInvariant());
|
||||
|
||||
private void invalidateCache() => fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache);
|
||||
private void invalidateCache()
|
||||
{
|
||||
fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache);
|
||||
CacheInvalidated?.Invoke();
|
||||
}
|
||||
|
||||
private Dictionary<string, string> initialiseFileCache() => liveSource.PerformRead(source =>
|
||||
{
|
||||
|
||||
@@ -63,6 +63,8 @@ namespace osu.Game.Skinning
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
protected IResourceStore<byte[]>? FallbackStore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new skin.
|
||||
/// </summary>
|
||||
@@ -102,6 +104,7 @@ namespace osu.Game.Skinning
|
||||
SkinInfo = skin.ToLiveUnmanaged();
|
||||
}
|
||||
|
||||
FallbackStore = fallbackStore;
|
||||
if (fallbackStore != null)
|
||||
store.AddStore(fallbackStore);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user