diff --git a/TRLevelControl/Control/TR4LevelControl.cs b/TRLevelControl/Control/TR4LevelControl.cs index a0870de08..75e82cdc6 100644 --- a/TRLevelControl/Control/TR4LevelControl.cs +++ b/TRLevelControl/Control/TR4LevelControl.cs @@ -113,6 +113,8 @@ private void ReadLevelDataChunk(TRLevelReader mainReader) ReadEntities(reader); + ReadDemoData(reader); + ReadSoundEffects(reader); reader.ReadUInt16s(3); // Always 0s @@ -144,6 +146,8 @@ private void WriteLevelDataChunk(TRLevelWriter mainWriter) WriteEntities(writer); + WriteDemoData(writer); + WriteSoundEffects(writer); writer.Write(Enumerable.Repeat((ushort)0, 3).ToArray()); @@ -383,59 +387,137 @@ private void WriteEntities(TRLevelWriter writer) writer.Write(_level.AIEntities); } - private void ReadSoundEffects(TRLevelReader reader) + private void ReadDemoData(TRLevelReader reader) { - TR4FileReadUtilities.PopulateDemoSoundSampleIndices(reader, _level); + ushort numDemoData = reader.ReadUInt16(); + _level.DemoData = reader.ReadBytes(numDemoData); } - private void WriteSoundEffects(TRLevelWriter writer) + private void WriteDemoData(TRLevelWriter writer) { writer.Write((ushort)_level.DemoData.Length); writer.Write(_level.DemoData); + } + + private void ReadSoundEffects(TRLevelReader reader) + { + _level.SoundEffects = new(); + short[] soundMap = reader.ReadInt16s(Enum.GetValues().Length); + + uint numSoundDetails = reader.ReadUInt32(); + List sfx = new(); - foreach (short sound in _level.SoundMap) + Dictionary sampleMap = new(); + for (int i = 0; i < numSoundDetails; i++) { - writer.Write(sound); + sampleMap[i] = reader.ReadUInt16(); + sfx.Add(new() + { + Volume = reader.ReadByte(), + Range = reader.ReadByte(), + Chance = reader.ReadByte(), + Pitch = reader.ReadByte(), + Samples = new() + }); + + sfx[i].SetFlags(reader.ReadUInt16()); } - writer.Write((uint)_level.SoundDetails.Count); - foreach (TR4SoundDetails snd in _level.SoundDetails) + // Sample indices are discarded in game. The details point to the samples + // directly per ReadWAVData. Observe the reads here only. + uint numSampleIndices = reader.ReadUInt32(); + uint[] sampleIndices = reader.ReadUInt32s(numSampleIndices); + _observer?.OnSampleIndicesRead(sampleIndices); + + for (int i = 0; i < soundMap.Length; i++) { - writer.Write(snd.Serialize()); + if (soundMap[i] < 0 || soundMap[i] >= sfx.Count) + { + continue; + } + + _level.SoundEffects[(TR4SFX)i] = sfx[soundMap[i]]; } + } - writer.Write((uint)_level.SampleIndices.Count); - foreach (uint sampleindex in _level.SampleIndices) + private void WriteSoundEffects(TRLevelWriter writer) + { + short detailsIndex = 0; + List sampleIndices = new(); + List samples = new(); + + foreach (TR4SFX id in Enum.GetValues()) + { + writer.Write(_level.SoundEffects.ContainsKey(id) ? detailsIndex++ : (short)-1); + } + + writer.Write((uint)_level.SoundEffects.Count); + foreach (TR4SoundEffect details in _level.SoundEffects.Values) { - writer.Write(sampleindex); + TR4Sample firstSample = details.Samples.First(); + int sampleIndex = samples.IndexOf(firstSample); + if (sampleIndex == -1) + { + sampleIndex = samples.Count; + samples.AddRange(details.Samples); + } + + writer.Write((ushort)sampleIndex); + writer.Write(details.Volume); + writer.Write(details.Range); + writer.Write(details.Chance); + writer.Write(details.Pitch); + writer.Write(details.GetFlags()); + + sampleIndices.Add((uint)sampleIndex); } + + // Sample indices are not required, but write them anyway to match OG + IEnumerable outputIndices = _observer?.GetSampleIndices() ?? sampleIndices; + writer.Write((uint)outputIndices.Count()); + writer.Write(outputIndices); } private void ReadWAVData(TRLevelReader reader) { uint numSamples = reader.ReadUInt32(); - _level.Samples = new(); + List samples = new(); for (int i = 0; i < numSamples; i++) { - _level.Samples.Add(new() + TR4Sample sample = new() { - UncompSize = reader.ReadUInt32(), - CompSize = reader.ReadUInt32(), - }); + InflatedLength = reader.ReadUInt32() + }; + samples.Add(sample); + + uint compressedSize = reader.ReadUInt32(); + sample.Data = reader.ReadUInt8s(compressedSize); + } - _level.Samples[i].CompressedChunk = reader.ReadBytes((int)_level.Samples[i].CompSize); + int pos = 0; + foreach (TR4SoundEffect sfx in _level.SoundEffects.Values) + { + for (int i = 0; i < sfx.Samples.Capacity; i++) + { + sfx.Samples.Add(samples[pos++]); + } } } private void WriteWAVData(TRLevelWriter writer) { - writer.Write((uint)_level.Samples.Count); - foreach (TR4Sample sample in _level.Samples) + List samples = _level.SoundEffects.Values + .SelectMany(s => s.Samples) + .Distinct() + .ToList(); + + writer.Write((uint)samples.Count); + foreach (TR4Sample sample in samples) { - writer.Write(sample.UncompSize); - writer.Write(sample.CompSize); - writer.Write(sample.CompressedChunk); + writer.Write(sample.InflatedLength); + writer.Write((uint)sample.Data.Length); + writer.Write(sample.Data); } } } diff --git a/TRLevelControl/Control/TR5LevelControl.cs b/TRLevelControl/Control/TR5LevelControl.cs index 1dcc34245..141bd8f82 100644 --- a/TRLevelControl/Control/TR5LevelControl.cs +++ b/TRLevelControl/Control/TR5LevelControl.cs @@ -129,10 +129,9 @@ private void ReadLevelDataChunk(TRLevelReader reader) ReadEntities(reader); - //reader.ReadUInt16(); // Unused, always 0 //IF we eliminate demodata + ReadDemoData(reader); ReadSoundEffects(reader); - ReadWAVData(reader); } private void WriteLevelDataChunk(TRLevelWriter mainWriter) @@ -160,7 +159,7 @@ private void WriteLevelDataChunk(TRLevelWriter mainWriter) WriteEntities(writer); - //writer.Write((ushort)0); //IF we eliminate demodata + WriteDemoData(writer); WriteSoundEffects(writer); @@ -407,62 +406,135 @@ private void WriteEntities(TRLevelWriter writer) writer.Write(_level.AIEntities); } - private void ReadSoundEffects(TRLevelReader reader) + private void ReadDemoData(TRLevelReader reader) { - TR5FileReadUtilities.PopulateDemoSoundSampleIndices(reader, _level); - reader.ReadBytes(6); // Always 0xCD + ushort numDemoData = reader.ReadUInt16(); + _level.DemoData = reader.ReadBytes(numDemoData); } - private void WriteSoundEffects(TRLevelWriter writer) + private void WriteDemoData(TRLevelWriter writer) { writer.Write((ushort)_level.DemoData.Length); writer.Write(_level.DemoData); + } - foreach (short sound in _level.SoundMap) + private void ReadSoundEffects(TRLevelReader reader) + { + _level.SoundEffects = new(); + short[] soundMap = reader.ReadInt16s(Enum.GetValues().Length); + + uint numSoundDetails = reader.ReadUInt32(); + List sfx = new(); + + Dictionary sampleMap = new(); + for (int i = 0; i < numSoundDetails; i++) { - writer.Write(sound); + sampleMap[i] = reader.ReadUInt16(); + sfx.Add(new() + { + Volume = reader.ReadByte(), + Range = reader.ReadByte(), + Chance = reader.ReadByte(), + Pitch = reader.ReadByte(), + Samples = new() + }); + + sfx[i].SetFlags(reader.ReadUInt16()); } - writer.Write((uint)_level.SoundDetails.Count); - foreach (TR4SoundDetails snd in _level.SoundDetails) + // Sample indices are discarded in game. The details point to the samples + // directly per ReadWAVData. Observe the reads here only. + uint numSampleIndices = reader.ReadUInt32(); + uint[] sampleIndices = reader.ReadUInt32s(numSampleIndices); + _observer?.OnSampleIndicesRead(sampleIndices); + + for (int i = 0; i < soundMap.Length; i++) { - writer.Write(snd.Serialize()); + if (soundMap[i] == -1) + continue; + + _level.SoundEffects[(TR5SFX)i] = sfx[soundMap[i]]; } - writer.Write((uint)_level.SampleIndices.Count); - foreach (uint sampleindex in _level.SampleIndices) + reader.ReadBytes(6); // OxCD padding + + uint numSamples = reader.ReadUInt32(); + List samples = new(); + + for (int i = 0; i < numSamples; i++) { - writer.Write(sampleindex); + TR4Sample sample = new() + { + InflatedLength = reader.ReadUInt32() + }; + samples.Add(sample); + + uint compressedSize = reader.ReadUInt32(); + sample.Data = reader.ReadUInt8s(compressedSize); } - writer.Write(Enumerable.Repeat((byte)0xCD, 6).ToArray()); + for (int i = 0; i < sfx.Count; i++) + { + TR4SoundEffect effect = sfx[i]; + for (int j = 0; j < effect.Samples.Capacity; j++) + { + effect.Samples.Add(samples[sampleMap[i] + j]); + } + } } - private void ReadWAVData(TRLevelReader reader) + private void WriteSoundEffects(TRLevelWriter writer) { - uint numSamples = reader.ReadUInt32(); - _level.Samples = new(); + short detailsIndex = 0; + List sampleIndices = new(); + List samples = new(); - for (int i = 0; i < numSamples; i++) + foreach (TR5SFX id in Enum.GetValues()) { - _level.Samples.Add(new() + writer.Write(_level.SoundEffects.ContainsKey(id) ? detailsIndex++ : (short)-1); + } + + writer.Write((uint)_level.SoundEffects.Count); + foreach (TR4SoundEffect details in _level.SoundEffects.Values) + { + TR4Sample firstSample = details.Samples.First(); + int sampleIndex = samples.IndexOf(firstSample); + if (sampleIndex == -1) { - UncompSize = reader.ReadUInt32(), - CompSize = reader.ReadUInt32(), - }); + sampleIndex = samples.Count; + samples.AddRange(details.Samples); + } - _level.Samples[i].CompressedChunk = reader.ReadBytes((int)_level.Samples[i].CompSize); + writer.Write((ushort)sampleIndex); + writer.Write(details.Volume); + writer.Write(details.Range); + writer.Write(details.Chance); + writer.Write(details.Pitch); + writer.Write(details.GetFlags()); + + sampleIndices.Add((uint)sampleIndex); } + + IEnumerable outputIndices = _observer?.GetSampleIndices() ?? sampleIndices; + writer.Write((uint)outputIndices.Count()); + writer.Write(outputIndices); + + writer.Write(Enumerable.Repeat((byte)0xCD, 6)); } private void WriteWAVData(TRLevelWriter writer) { - writer.Write((uint)_level.Samples.Count); - foreach (TR4Sample sample in _level.Samples) + List samples = _level.SoundEffects.Values + .SelectMany(s => s.Samples) + .Distinct() + .ToList(); + + writer.Write((uint)samples.Count); + foreach (TR4Sample sample in samples) { - writer.Write(sample.UncompSize); - writer.Write(sample.CompSize); - writer.Write(sample.CompressedChunk); + writer.Write(sample.InflatedLength); + writer.Write((uint)sample.Data.Length); + writer.Write(sample.Data); } } } diff --git a/TRLevelControl/ITRLevelObserver.cs b/TRLevelControl/ITRLevelObserver.cs index 2bcd9cc7d..0740dd2b4 100644 --- a/TRLevelControl/ITRLevelObserver.cs +++ b/TRLevelControl/ITRLevelObserver.cs @@ -8,4 +8,6 @@ public interface ITRLevelObserver void OnChunkWritten(long startPosition, long endPosition, TRChunkType chunkType, byte[] data); void OnMeshPaddingRead(uint meshPointer, List values); List GetMeshPadding(uint meshPointer); + void OnSampleIndicesRead(uint[] sampleIndices); + IEnumerable GetSampleIndices(); } diff --git a/TRLevelControl/Model/TR4/TR4Level.cs b/TRLevelControl/Model/TR4/TR4Level.cs index 1f0baef81..fac1c8c41 100644 --- a/TRLevelControl/Model/TR4/TR4Level.cs +++ b/TRLevelControl/Model/TR4/TR4Level.cs @@ -29,8 +29,5 @@ public class TR4Level : TRLevelBase public List Entities { get; set; } public List AIEntities { get; set; } public byte[] DemoData { get; set; } - public short[] SoundMap { get; set; } - public List SoundDetails { get; set; } - public List SampleIndices { get; set; } - public List Samples { get; set; } + public SortedDictionary SoundEffects { get; set; } } diff --git a/TRLevelControl/Model/TR4/TR4Sample.cs b/TRLevelControl/Model/TR4/TR4Sample.cs index 0292bdee9..6b74d0b52 100644 --- a/TRLevelControl/Model/TR4/TR4Sample.cs +++ b/TRLevelControl/Model/TR4/TR4Sample.cs @@ -1,22 +1,7 @@ -using TRLevelControl.Serialization; +namespace TRLevelControl.Model; -namespace TRLevelControl.Model; - -public class TR4Sample : ISerializableCompact +public class TR4Sample { - public uint UncompSize { get; set; } - - public uint CompSize { get; set; } - - public byte[] SoundData { get; set; } - - //Optional - mainly just for testing, this is just to store the raw zlib compressed chunk. - public byte[] CompressedChunk { get; set; } - - public byte[] Serialize() - { - //we cheat a bit here - sample is not actually zlib compressed, it is simply a WAV file. - //So in the TR4Level file we will write the sizes and compressed chunk straight. - throw new NotImplementedException(); - } + public uint InflatedLength { get; set; } + public byte[] Data { get; set; } } diff --git a/TRLevelControl/Model/TR4/TR4SoundEffect.cs b/TRLevelControl/Model/TR4/TR4SoundEffect.cs new file mode 100644 index 000000000..bc92f3ca3 --- /dev/null +++ b/TRLevelControl/Model/TR4/TR4SoundEffect.cs @@ -0,0 +1,16 @@ +namespace TRLevelControl.Model; + +public class TR4SoundEffect : TRSoundEffect +{ + public byte Volume { get; set; } + public byte Chance { get; set; } + public byte Range { get; set; } + public byte Pitch { get; set; } + public List Samples { get; set; } + + protected override void SetSampleCount(int count) + => Samples.Capacity = count; + + protected override int GetSampleCount() + => Samples.Count; +} diff --git a/TRLevelControl/Model/TR5/TR5Level.cs b/TRLevelControl/Model/TR5/TR5Level.cs index b19c5bc84..5475b2c27 100644 --- a/TRLevelControl/Model/TR5/TR5Level.cs +++ b/TRLevelControl/Model/TR5/TR5Level.cs @@ -31,8 +31,5 @@ public class TR5Level : TRLevelBase public List Entities { get; set; } public List AIEntities { get; set; } public byte[] DemoData { get; set; } - public short[] SoundMap { get; set; } - public List SoundDetails { get; set; } - public List SampleIndices { get; set; } - public List Samples { get; set; } + public SortedDictionary SoundEffects { get; set; } } diff --git a/TRLevelControl/TR4FileReadUtilities.cs b/TRLevelControl/TR4FileReadUtilities.cs index 440ecb8de..e382bd6c8 100644 --- a/TRLevelControl/TR4FileReadUtilities.cs +++ b/TRLevelControl/TR4FileReadUtilities.cs @@ -290,36 +290,6 @@ public static void PopulateEntitiesAndAI(TRLevelReader reader, TR4Level lvl) lvl.AIEntities = reader.ReadTR4AIEntities(numEntities); } - public static void PopulateDemoSoundSampleIndices(BinaryReader reader, TR4Level lvl) - { - ushort numDemoData = reader.ReadUInt16(); - lvl.DemoData = reader.ReadBytes(numDemoData); - - //Sound Map (370 shorts) & Sound Details - lvl.SoundMap = new short[370]; - - for (int i = 0; i < lvl.SoundMap.Length; i++) - { - lvl.SoundMap[i] = reader.ReadInt16(); - } - - uint numSoundDetails = reader.ReadUInt32(); - lvl.SoundDetails = new(); - - for (int i = 0; i < numSoundDetails; i++) - { - lvl.SoundDetails.Add(ReadSoundDetails(reader)); - } - - uint numSampleIndices = reader.ReadUInt32(); - lvl.SampleIndices = new(); - - for (int i = 0; i < numSampleIndices; i++) - { - lvl.SampleIndices.Add(reader.ReadUInt32()); - } - } - public static TR4SoundDetails ReadSoundDetails(BinaryReader reader) { return new() diff --git a/TRLevelControl/TR5FileReadUtilities.cs b/TRLevelControl/TR5FileReadUtilities.cs index d168ed6d4..b1cade44f 100644 --- a/TRLevelControl/TR5FileReadUtilities.cs +++ b/TRLevelControl/TR5FileReadUtilities.cs @@ -463,34 +463,4 @@ public static void PopulateEntitiesAndAI(TRLevelReader reader, TR5Level lvl) numEntities = reader.ReadUInt32(); lvl.AIEntities = reader.ReadTR5AIEntities(numEntities); } - - public static void PopulateDemoSoundSampleIndices(BinaryReader reader, TR5Level lvl) - { - ushort numDemoData = reader.ReadUInt16(); - lvl.DemoData = reader.ReadBytes(numDemoData); - - //Sound Map (370 shorts) & Sound Details - lvl.SoundMap = new short[450]; - - for (int i = 0; i < lvl.SoundMap.Length; i++) - { - lvl.SoundMap[i] = reader.ReadInt16(); - } - - uint numSoundDetails = reader.ReadUInt32(); - lvl.SoundDetails = new(); - - for (int i = 0; i < numSoundDetails; i++) - { - lvl.SoundDetails.Add(TR4FileReadUtilities.ReadSoundDetails(reader)); - } - - uint numSampleIndices = reader.ReadUInt32(); - lvl.SampleIndices = new(); - - for (int i = 0; i < numSampleIndices; i++) - { - lvl.SampleIndices.Add(reader.ReadUInt32()); - } - } } diff --git a/TRLevelControlTests/Base/Observers/ObserverBase.cs b/TRLevelControlTests/Base/Observers/ObserverBase.cs index cd96919c2..8e899c830 100644 --- a/TRLevelControlTests/Base/Observers/ObserverBase.cs +++ b/TRLevelControlTests/Base/Observers/ObserverBase.cs @@ -22,4 +22,10 @@ public virtual void OnMeshPaddingRead(uint meshPointer, List values) public virtual List GetMeshPadding(uint meshPointer) => null; + + public virtual void OnSampleIndicesRead(uint[] sampleIndices) + { } + + public virtual IEnumerable GetSampleIndices() + => null; } diff --git a/TRLevelControlTests/Base/Observers/TR45Observer.cs b/TRLevelControlTests/Base/Observers/TR45Observer.cs index abf0e53e0..e7d568d12 100644 --- a/TRLevelControlTests/Base/Observers/TR45Observer.cs +++ b/TRLevelControlTests/Base/Observers/TR45Observer.cs @@ -10,6 +10,8 @@ public class TR45Observer : ObserverBase private readonly Dictionary> _meshPadding = new(); + private uint[] _sampleIndices; + public override void TestOutput(byte[] input, byte[] output) { CollectionAssert.AreEquivalent(_inflatedReads.Keys, _inflatedWrites.Keys); @@ -71,6 +73,16 @@ public override List GetMeshPadding(uint meshPointer) return _meshPadding.ContainsKey(meshPointer) ? _meshPadding[meshPointer] : null; } + public override void OnSampleIndicesRead(uint[] sampleIndices) + { + _sampleIndices = sampleIndices; + } + + public override IEnumerable GetSampleIndices() + { + return _sampleIndices; + } + class ZipWrapper { public long StreamStart { get; set; }