diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index d3c61960bb..ad203d2107 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -230,5 +230,23 @@ namespace osu.Game.Tests.Beatmaps.Formats
SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(new SampleInfo { Name = "hitnormal" });
}
+
+ [Test]
+ public void TestDecodeCustomHitObjectSamples()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+ using (var resStream = Resource.OpenResource("custom-hitobject-samples.osu"))
+ using (var stream = new StreamReader(resStream))
+ {
+ var hitObjects = decoder.Decode(stream).HitObjects;
+
+ Assert.AreEqual("hit_1.wav", hitObjects[0].Samples[0].LookupNames.First());
+ Assert.AreEqual("hit_2.wav", hitObjects[1].Samples[0].LookupNames.First());
+ Assert.AreEqual("hitnormal2", getTestableSampleInfo(hitObjects[2]).Name);
+ Assert.AreEqual("hit_1.wav", hitObjects[3].Samples[0].LookupNames.First());
+ }
+
+ SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(new SampleInfo { Name = "hitnormal" });
+ }
}
}
diff --git a/osu.Game.Tests/Resources/custom-hitobject-samples.osu b/osu.Game.Tests/Resources/custom-hitobject-samples.osu
new file mode 100644
index 0000000000..588672e2d9
--- /dev/null
+++ b/osu.Game.Tests/Resources/custom-hitobject-samples.osu
@@ -0,0 +1,16 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+
+[TimingPoints]
+2170,468.75,4,1,0,40,1,0
+2638,-100,4,1,1,40,0,0
+3107,-100,4,1,2,40,0,0
+3576,-100,4,1,0,40,0,0
+
+[HitObjects]
+255,193,2170,1,0,0:0:0:0:hit_1.wav
+256,191,2638,5,0,0:0:0:0:hit_2.wav
+255,193,3107,1,0,0:0:0:0:
+256,191,3576,1,0,0:0:0:0:hit_1.wav
diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs
index 53b6e439f5..7e329ac651 100644
--- a/osu.Game/Audio/SampleInfo.cs
+++ b/osu.Game/Audio/SampleInfo.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
+using System.Collections.Generic;
namespace osu.Game.Audio
{
@@ -33,6 +34,20 @@ namespace osu.Game.Audio
///
public int Volume;
+ ///
+ /// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first).
+ ///
+ public virtual IEnumerable LookupNames
+ {
+ get
+ {
+ if (!string.IsNullOrEmpty(Namespace))
+ yield return $"{Namespace}/{Bank}-{Name}";
+
+ yield return $"{Bank}-{Name}"; // Without namespace as a fallback even when we have a namespace
+ }
+ }
+
public SampleInfo Clone() => (SampleInfo)MemberwiseClone();
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 9edd0f1f34..95589d8953 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -6,6 +6,7 @@ using osu.Game.Rulesets.Objects.Types;
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.IO;
using osu.Game.Beatmaps.Formats;
using osu.Game.Audio;
using System.Linq;
@@ -196,9 +197,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
var bank = (LegacyBeatmapDecoder.LegacySampleBank)Convert.ToInt32(split[0]);
var addbank = (LegacyBeatmapDecoder.LegacySampleBank)Convert.ToInt32(split[1]);
- // Let's not implement this for now, because this doesn't fit nicely into the bank structure
- //string sampleFile = split2.Length > 4 ? split2[4] : string.Empty;
-
string stringBank = bank.ToString().ToLower();
if (stringBank == @"none")
stringBank = null;
@@ -211,6 +209,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (split.Length > 3)
bankInfo.Volume = int.Parse(split[3]);
+
+ bankInfo.Filename = split.Length > 4 ? split[4] : null;
}
///
@@ -252,6 +252,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
private List convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
{
+ // 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))
+ return new List { new FileSampleInfo { Filename = bankInfo.Filename } };
+
var soundTypes = new List
{
new SampleInfo
@@ -297,14 +301,24 @@ namespace osu.Game.Rulesets.Objects.Legacy
private class SampleBankInfo
{
+ public string Filename;
+
public string Normal;
public string Add;
public int Volume;
- public SampleBankInfo Clone()
+ public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
+ }
+
+ private class FileSampleInfo : SampleInfo
+ {
+ public string Filename;
+
+ public override IEnumerable LookupNames => new[]
{
- return (SampleBankInfo)MemberwiseClone();
- }
+ Filename,
+ Path.ChangeExtension(Filename, null)
+ };
}
[Flags]
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index 1a11b0c03e..2fc9cff463 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -44,19 +44,17 @@ namespace osu.Game.Skinning
private SampleChannel loadChannel(SampleInfo info, Func getSampleFunction)
{
- SampleChannel ch = null;
+ foreach (var lookup in info.LookupNames)
+ {
+ var ch = getSampleFunction($"Gameplay/{lookup}");
+ if (ch == null)
+ continue;
- if (info.Namespace != null)
- ch = getSampleFunction($"Gameplay/{info.Namespace}/{info.Bank}-{info.Name}");
-
- // try without namespace as a fallback.
- if (ch == null)
- ch = getSampleFunction($"Gameplay/{info.Bank}-{info.Name}");
-
- if (ch != null)
ch.Volume.Value = info.Volume / 100.0;
+ return ch;
+ }
- return ch;
+ return null;
}
}
}