1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-21 20:53:04 +08:00

Merge pull request #11033 from smoogipoo/immutable-hit-samples

Make HitSampleInfo immutable
This commit is contained in:
Dean Herbert 2020-12-02 14:44:26 +09:00 committed by GitHub
commit 71a121389b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 183 additions and 132 deletions

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests
NewCombo = i % 8 == 0, NewCombo = i % 8 == 0,
Samples = new List<HitSampleInfo>(new[] Samples = new List<HitSampleInfo>(new[]
{ {
new HitSampleInfo { Bank = "normal", Name = "hitnormal", Volume = 100 } new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 100)
}) })
}); });
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
@ -50,11 +51,24 @@ namespace osu.Game.Rulesets.Catch.Objects
} }
} }
private class BananaHitSampleInfo : HitSampleInfo private class BananaHitSampleInfo : HitSampleInfo, IEquatable<BananaHitSampleInfo>
{ {
private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; private static readonly string[] lookup_names = { "metronomelow", "catch-banana" };
public override IEnumerable<string> LookupNames => lookupNames; public override IEnumerable<string> LookupNames => lookup_names;
public BananaHitSampleInfo()
: base(string.Empty)
{
}
public bool Equals(BananaHitSampleInfo other)
=> other != null;
public override bool Equals(object obj)
=> obj is BananaHitSampleInfo other && Equals(other);
public override int GetHashCode() => lookup_names.GetHashCode();
} }
} }
} }

View File

@ -50,12 +50,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
base.CreateNestedHitObjects(cancellationToken); base.CreateNestedHitObjects(cancellationToken);
var dropletSamples = Samples.Select(s => new HitSampleInfo var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList();
{
Bank = s.Bank,
Name = @"slidertick",
Volume = s.Volume
}).ToList();
int nodeIndex = 0; int nodeIndex = 0;
SliderEventDescriptor? lastEvent = null; SliderEventDescriptor? lastEvent = null;

View File

@ -108,8 +108,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("change samples", () => slider.HitObject.Samples = new[] AddStep("change samples", () => slider.HitObject.Samples = new[]
{ {
new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }, new HitSampleInfo(HitSampleInfo.HIT_CLAP),
new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE }, new HitSampleInfo(HitSampleInfo.HIT_WHISTLE),
}); });
AddAssert("head samples updated", () => assertSamples(slider.HitObject.HeadCircle)); AddAssert("head samples updated", () => assertSamples(slider.HitObject.HeadCircle));
@ -136,15 +136,15 @@ namespace osu.Game.Rulesets.Osu.Tests
slider = (DrawableSlider)createSlider(repeats: 1); slider = (DrawableSlider)createSlider(repeats: 1);
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
slider.HitObject.NodeSamples.Add(new List<HitSampleInfo> { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } }); slider.HitObject.NodeSamples.Add(new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_FINISH) });
Add(slider); Add(slider);
}); });
AddStep("change samples", () => slider.HitObject.Samples = new[] AddStep("change samples", () => slider.HitObject.Samples = new[]
{ {
new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }, new HitSampleInfo(HitSampleInfo.HIT_CLAP),
new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE }, new HitSampleInfo(HitSampleInfo.HIT_WHISTLE),
}); });
AddAssert("head samples not updated", () => assertSamples(slider.HitObject.HeadCircle)); AddAssert("head samples not updated", () => assertSamples(slider.HitObject.HeadCircle));

View File

@ -115,8 +115,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (firstSample != null) if (firstSample != null)
{ {
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide");
clone.Name = "sliderslide";
samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone) samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone)
{ {

View File

@ -110,8 +110,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (firstSample != null) if (firstSample != null)
{ {
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin");
clone.Name = "spinnerspin";
samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone) samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone)
{ {

View File

@ -221,14 +221,7 @@ namespace osu.Game.Rulesets.Osu.Objects
var sampleList = new List<HitSampleInfo>(); var sampleList = new List<HitSampleInfo>();
if (firstSample != null) if (firstSample != null)
{ sampleList.Add(firstSample.With("slidertick"));
sampleList.Add(new HitSampleInfo
{
Bank = firstSample.Bank,
Volume = firstSample.Volume,
Name = @"slidertick",
});
}
foreach (var tick in NestedHitObjects.OfType<SliderTick>()) foreach (var tick in NestedHitObjects.OfType<SliderTick>())
tick.Samples = sampleList; tick.Samples = sampleList;

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
public SpinnerBonusTick() public SpinnerBonusTick()
{ {
Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); Samples.Add(new HitSampleInfo("spinnerbonus"));
} }
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement(); public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();

View File

@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (isRimType != rimSamples.Any()) if (isRimType != rimSamples.Any())
{ {
if (isRimType) if (isRimType)
HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }); HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
else else
{ {
foreach (var sample in rimSamples) foreach (var sample in rimSamples)
@ -125,9 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (s.Name != HitSampleInfo.HIT_FINISH) if (s.Name != HitSampleInfo.HIT_FINISH)
continue; continue;
var sClone = s.Clone(); corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE);
sClone.Name = HitSampleInfo.HIT_WHISTLE;
corrected[i] = sClone;
} }
return corrected; return corrected;

View File

@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (isStrong.Value != strongSamples.Any()) if (isStrong.Value != strongSamples.Any())
{ {
if (isStrong.Value) if (isStrong.Value)
HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH }); HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
else else
{ {
foreach (var sample in strongSamples) foreach (var sample in strongSamples)

View File

@ -139,7 +139,7 @@ namespace osu.Game.Tests.Editing
HitObjects = HitObjects =
{ {
(OsuHitObject)current.HitObjects[0], (OsuHitObject)current.HitObjects[0],
new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_FINISH) } },
(OsuHitObject)current.HitObjects[2], (OsuHitObject)current.HitObjects[2],
} }
}; };
@ -268,12 +268,12 @@ namespace osu.Game.Tests.Editing
HitObjects = HitObjects =
{ {
(OsuHitObject)current.HitObjects[0], (OsuHitObject)current.HitObjects[0],
new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_FINISH) } },
(OsuHitObject)current.HitObjects[2], (OsuHitObject)current.HitObjects[2],
(OsuHitObject)current.HitObjects[3], (OsuHitObject)current.HitObjects[3],
new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE } } }, new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) } },
(OsuHitObject)current.HitObjects[5], (OsuHitObject)current.HitObjects[5],
new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP } } }, new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_CLAP) } },
(OsuHitObject)current.HitObjects[7], (OsuHitObject)current.HitObjects[7],
} }
}; };

View File

@ -1,8 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Utils;
namespace osu.Game.Audio namespace osu.Game.Audio
{ {
@ -10,7 +14,7 @@ namespace osu.Game.Audio
/// Describes a gameplay hit sample. /// Describes a gameplay hit sample.
/// </summary> /// </summary>
[Serializable] [Serializable]
public class HitSampleInfo : ISampleInfo public class HitSampleInfo : ISampleInfo, IEquatable<HitSampleInfo>
{ {
public const string HIT_WHISTLE = @"hitwhistle"; public const string HIT_WHISTLE = @"hitwhistle";
public const string HIT_FINISH = @"hitfinish"; public const string HIT_FINISH = @"hitfinish";
@ -22,29 +26,38 @@ namespace osu.Game.Audio
/// </summary> /// </summary>
public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH }; public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH };
/// <summary>
/// The bank to load the sample from.
/// </summary>
public string Bank;
/// <summary> /// <summary>
/// The name of the sample to load. /// The name of the sample to load.
/// </summary> /// </summary>
public string Name; public readonly string Name;
/// <summary>
/// The bank to load the sample from.
/// </summary>
public readonly string? Bank;
/// <summary> /// <summary>
/// An optional suffix to provide priority lookup. Falls back to non-suffixed <see cref="Name"/>. /// An optional suffix to provide priority lookup. Falls back to non-suffixed <see cref="Name"/>.
/// </summary> /// </summary>
public string Suffix; public readonly string? Suffix;
/// <summary> /// <summary>
/// The sample volume. /// The sample volume.
/// </summary> /// </summary>
public int Volume { get; set; } public int Volume { get; }
public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 0)
{
Name = name;
Bank = bank;
Suffix = suffix;
Volume = volume;
}
/// <summary> /// <summary>
/// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first). /// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first).
/// </summary> /// </summary>
[JsonIgnore]
public virtual IEnumerable<string> LookupNames public virtual IEnumerable<string> LookupNames
{ {
get get
@ -56,6 +69,23 @@ namespace osu.Game.Audio
} }
} }
public HitSampleInfo Clone() => (HitSampleInfo)MemberwiseClone(); /// <summary>
/// Creates a new <see cref="HitSampleInfo"/> with overridden values.
/// </summary>
/// <param name="newName">An optional new sample name.</param>
/// <param name="newBank">An optional new sample bank.</param>
/// <param name="newSuffix">An optional new lookup suffix.</param>
/// <param name="newVolume">An optional new volume.</param>
/// <returns>The new <see cref="HitSampleInfo"/>.</returns>
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume));
public bool Equals(HitSampleInfo? other)
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;
public override bool Equals(object? obj)
=> obj is HitSampleInfo other && Equals(other);
public override int GetHashCode() => HashCode.Combine(Name, Bank, Suffix);
} }
} }

View File

@ -58,12 +58,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
/// <param name="sampleName">The name of the same.</param> /// <param name="sampleName">The name of the same.</param>
/// <returns>A populated <see cref="HitSampleInfo"/>.</returns> /// <returns>A populated <see cref="HitSampleInfo"/>.</returns>
public HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) => new HitSampleInfo public HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) => new HitSampleInfo(sampleName, SampleBank, volume: SampleVolume);
{
Bank = SampleBank,
Name = sampleName,
Volume = SampleVolume,
};
/// <summary> /// <summary>
/// Applies <see cref="SampleBank"/> and <see cref="SampleVolume"/> to a <see cref="HitSampleInfo"/> if necessary, returning the modified <see cref="HitSampleInfo"/>. /// Applies <see cref="SampleBank"/> and <see cref="SampleVolume"/> to a <see cref="HitSampleInfo"/> if necessary, returning the modified <see cref="HitSampleInfo"/>.
@ -71,12 +66,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="hitSampleInfo">The <see cref="HitSampleInfo"/>. This will not be modified.</param> /// <param name="hitSampleInfo">The <see cref="HitSampleInfo"/>. This will not be modified.</param>
/// <returns>The modified <see cref="HitSampleInfo"/>. This does not share a reference with <paramref name="hitSampleInfo"/>.</returns> /// <returns>The modified <see cref="HitSampleInfo"/>. This does not share a reference with <paramref name="hitSampleInfo"/>.</returns>
public virtual HitSampleInfo ApplyTo(HitSampleInfo hitSampleInfo) public virtual HitSampleInfo ApplyTo(HitSampleInfo hitSampleInfo)
{ => hitSampleInfo.With(newBank: hitSampleInfo.Bank ?? SampleBank, newVolume: hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume);
var newSampleInfo = hitSampleInfo.Clone();
newSampleInfo.Bank = hitSampleInfo.Bank ?? SampleBank;
newSampleInfo.Volume = hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume;
return newSampleInfo;
}
public override bool IsRedundant(ControlPoint existing) public override bool IsRedundant(ControlPoint existing)
=> existing is SampleControlPoint existingSample => existing is SampleControlPoint existingSample

View File

@ -192,7 +192,7 @@ namespace osu.Game.Beatmaps.Formats
var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time);
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo()); HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty));
// Convert effect flags to the legacy format // Convert effect flags to the legacy format
LegacyEffectFlags effectFlags = LegacyEffectFlags.None; LegacyEffectFlags effectFlags = LegacyEffectFlags.None;

View File

@ -182,11 +182,8 @@ namespace osu.Game.Beatmaps.Formats
{ {
var baseInfo = base.ApplyTo(hitSampleInfo); var baseInfo = base.ApplyTo(hitSampleInfo);
if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0)
&& legacy.CustomSampleBank == 0) return legacy.With(newCustomSampleBank: CustomSampleBank);
{
legacy.CustomSampleBank = CustomSampleBank;
}
return baseInfo; return baseInfo;
} }

View File

@ -13,6 +13,7 @@ using JetBrains.Annotations;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Objects.Legacy namespace osu.Game.Rulesets.Objects.Legacy
{ {
@ -427,62 +428,25 @@ namespace osu.Game.Rulesets.Objects.Legacy
// Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario
if (!string.IsNullOrEmpty(bankInfo.Filename)) if (!string.IsNullOrEmpty(bankInfo.Filename))
{ {
return new List<HitSampleInfo> return new List<HitSampleInfo> { new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume) };
{
new FileHitSampleInfo
{
Filename = bankInfo.Filename,
Volume = bankInfo.Volume
}
};
} }
var soundTypes = new List<HitSampleInfo> var soundTypes = new List<HitSampleInfo>
{ {
new LegacyHitSampleInfo new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.Normal, bankInfo.Volume, bankInfo.CustomSampleBank,
{
Bank = bankInfo.Normal,
Name = HitSampleInfo.HIT_NORMAL,
Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
IsLayered = type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal) type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal))
}
}; };
if (type.HasFlag(LegacyHitSoundType.Finish)) if (type.HasFlag(LegacyHitSoundType.Finish))
{ soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
soundTypes.Add(new LegacyHitSampleInfo
{
Bank = bankInfo.Add,
Name = HitSampleInfo.HIT_FINISH,
Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank
});
}
if (type.HasFlag(LegacyHitSoundType.Whistle)) if (type.HasFlag(LegacyHitSoundType.Whistle))
{ soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
soundTypes.Add(new LegacyHitSampleInfo
{
Bank = bankInfo.Add,
Name = HitSampleInfo.HIT_WHISTLE,
Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank
});
}
if (type.HasFlag(LegacyHitSoundType.Clap)) if (type.HasFlag(LegacyHitSoundType.Clap))
{ soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
soundTypes.Add(new LegacyHitSampleInfo
{
Bank = bankInfo.Add,
Name = HitSampleInfo.HIT_CLAP,
Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank
});
}
return soundTypes; return soundTypes;
} }
@ -500,21 +464,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
} }
public class LegacyHitSampleInfo : HitSampleInfo #nullable enable
public class LegacyHitSampleInfo : HitSampleInfo, IEquatable<LegacyHitSampleInfo>
{ {
private int customSampleBank; public readonly int CustomSampleBank;
public int CustomSampleBank
{
get => customSampleBank;
set
{
customSampleBank = value;
if (value >= 2)
Suffix = value.ToString();
}
}
/// <summary> /// <summary>
/// Whether this hit sample is layered. /// Whether this hit sample is layered.
@ -523,18 +477,41 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled /// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled
/// using the <see cref="LegacySkinConfiguration.LegacySetting.LayeredHitSounds"/> skin config option. /// using the <see cref="LegacySkinConfiguration.LegacySetting.LayeredHitSounds"/> skin config option.
/// </remarks> /// </remarks>
public bool IsLayered { get; set; } public readonly bool IsLayered;
public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, int customSampleBank = 0, bool isLayered = false)
: base(name, bank, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume)
{
CustomSampleBank = customSampleBank;
IsLayered = isLayered;
}
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
=> With(newName, newBank, newVolume);
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default, Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default)
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
public bool Equals(LegacyHitSampleInfo? other)
=> base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered;
public override bool Equals(object? obj)
=> obj is LegacyHitSampleInfo other && Equals(other);
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered);
} }
private class FileHitSampleInfo : LegacyHitSampleInfo private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
{ {
public string Filename; public readonly string Filename;
public FileHitSampleInfo() public FileHitSampleInfo(string filename, int volume)
{ // Force CSS=1 to make sure that the LegacyBeatmapSkin does not fall back to the user skin.
// Make sure that the LegacyBeatmapSkin does not fall back to the user skin.
// Note that this does not change the lookup names, as they are overridden locally. // Note that this does not change the lookup names, as they are overridden locally.
CustomSampleBank = 1; : base(string.Empty, customSampleBank: 1, volume: volume)
{
Filename = filename;
} }
public override IEnumerable<string> LookupNames => new[] public override IEnumerable<string> LookupNames => new[]
@ -542,6 +519,20 @@ namespace osu.Game.Rulesets.Objects.Legacy
Filename, Filename,
Path.ChangeExtension(Filename, null) Path.ChangeExtension(Filename, null)
}; };
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string?> newBank = default, Optional<int> newVolume = default, Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default)
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));
public bool Equals(FileHitSampleInfo? other)
=> base.Equals(other) && Filename == other.Filename;
public override bool Equals(object? obj)
=> obj is FileHitSampleInfo other && Equals(other);
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Filename);
} }
#nullable disable
} }
} }

View File

@ -101,7 +101,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
case TernaryState.True: case TernaryState.True:
if (existingSample == null) if (existingSample == null)
samples.Add(new HitSampleInfo { Name = sampleName }); samples.Add(new HitSampleInfo(sampleName));
break; break;
} }
} }
@ -212,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (blueprint != null) if (blueprint != null)
{ {
// doing this post-creations as adding the default hit sample should be the case regardless of the ruleset. // doing this post-creations as adding the default hit sample should be the case regardless of the ruleset.
blueprint.HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL }); blueprint.HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
placementBlueprintContainer.Child = currentPlacement = blueprint; placementBlueprintContainer.Child = currentPlacement = blueprint;

View File

@ -328,7 +328,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (h.Samples.Any(s => s.Name == sampleName)) if (h.Samples.Any(s => s.Name == sampleName))
continue; continue;
h.Samples.Add(new HitSampleInfo { Name = sampleName }); h.Samples.Add(new HitSampleInfo(sampleName));
} }
EditorBeatmap.EndChange(); EditorBeatmap.EndChange();

View File

@ -0,0 +1,45 @@
// 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.
#nullable enable
namespace osu.Game.Utils
{
/// <summary>
/// A wrapper over a value and a boolean denoting whether the value is valid.
/// </summary>
/// <typeparam name="T">The type of value stored.</typeparam>
public readonly ref struct Optional<T>
{
/// <summary>
/// The stored value.
/// </summary>
public readonly T Value;
/// <summary>
/// Whether <see cref="Value"/> is valid.
/// </summary>
/// <remarks>
/// If <typeparamref name="T"/> is a reference type, <c>null</c> may be valid for <see cref="Value"/>.
/// </remarks>
public readonly bool HasValue;
private Optional(T value)
{
Value = value;
HasValue = true;
}
/// <summary>
/// Returns <see cref="Value"/> if it's valid, or a given fallback value otherwise.
/// </summary>
/// <remarks>
/// Shortcase for: <c>optional.HasValue ? optional.Value : fallback</c>.
/// </remarks>
/// <param name="fallback">The fallback value to return if <see cref="HasValue"/> is <c>false</c>.</param>
/// <returns></returns>
public T GetOr(T fallback) => HasValue ? Value : fallback;
public static implicit operator Optional<T>(T value) => new Optional<T>(value);
}
}