Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions TRLevelControl/IO/TRXInjector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;
using TRLevelControl.Model;
using TRLevelControl.Model.TRX;

namespace TRLevelControl.IO;

public static class TRXInjector
{
private const uint _magic = 'T' | 'R' << 8 | 'X' << 16 | 'J' << 24;
private const uint _version = 4;

public static TRXInjectionData Read(TRLevelReader reader)
{
try
{
if (reader.ReadUInt32() != _magic)
{
return null;
}
}
catch (EndOfStreamException)
{
return null;
}

// Skip version and config option value
reader.BaseStream.Position += 2 * sizeof(uint);

var zipReader = reader.Inflate(TRChunkType.LevelData);

// Ignore tests
zipReader.BaseStream.Position += sizeof(int);
var testLength = zipReader.ReadInt32();
zipReader.BaseStream.Position += testLength;

var chunkCount = zipReader.ReadInt32();
var data = new TRXInjectionData();
for (int i = 0; i < chunkCount; i++)
{
var chunk = TRXChunk.Read(zipReader);
using var chunkMS = new MemoryStream(chunk.Data);
using var chunkReader = new TRLevelReader(chunkMS);
ReadChunk(data, chunk, chunkReader);
}

return data;
}

private static void ReadChunk(TRXInjectionData data, TRXChunk chunk, TRLevelReader reader)
{
for (int i = 0; i < chunk.BlockCount; i++)
{
var blockType = (TRXBlockType)reader.ReadInt32();
var blockCount = reader.ReadInt32();
reader.BaseStream.Position += sizeof(int); // Skip total length

for (int j = 0; j < blockCount; j++)
{
switch (blockType)
{
case TRXBlockType.SampleInfos:
data.SFX.Add(TRSFXData.Read(reader));
break;
}
}
}
}

public static void Write(TRXInjectionData data, TRLevelWriter outWriter)
{
var chunks = Chunkify(data);
if (chunks == null || chunks.Count == 0)
{
return;
}

using var injStream = new MemoryStream();
using var injWriter = new TRLevelWriter(injStream);

injWriter.Write(0); // Number of tests
injWriter.Write(0); // Total length of tests
injWriter.Write(chunks.Count);
chunks.ForEach(c => c.Serialize(injWriter));

var inflatedData = injStream.ToArray();
using var outStream = new MemoryStream();
using var deflater = new DeflaterOutputStream(outStream);
using var inStream = new MemoryStream(inflatedData);

inStream.CopyTo(deflater);
deflater.Finish();

var deflatedData = outStream.ToArray();

outWriter.Write(_magic);
outWriter.Write(_version);
outWriter.Write(0); // Normally a value to link to a config option

outWriter.Write(inflatedData.Length);
outWriter.Write(deflatedData.Length);
outWriter.Write(deflatedData);
}

private static List<TRXChunk> Chunkify(TRXInjectionData data)
{
if (data == null)
{
return null;
}

var chunks = new List<TRXChunk>
{
CreateChunk(TRXChunkType.SFXData, data, WriteSFXData),
};

chunks.RemoveAll(c => c.BlockCount == 0);
return chunks;
}

private static TRXChunk CreateChunk(TRXChunkType type,
TRXInjectionData data, Func<TRXInjectionData, TRLevelWriter, int> process)
{
using var stream = new MemoryStream();
using var writer = new TRLevelWriter(stream);
int blockCount = process(data, writer);

return new()
{
Type = type,
BlockCount = blockCount,
Data = stream.ToArray(),
};
}

private static int WriteSFXData(TRXInjectionData data, TRLevelWriter writer)
{
return WriteBlock(TRXBlockType.SampleInfos, data.SFX.Count, writer,
s => data.SFX.ForEach(f => f.Write(s)));
}

private static int WriteBlock(TRXBlockType type, int elementCount,
TRLevelWriter writer, Action<TRLevelWriter> subCallback)
{
if (elementCount == 0)
{
return 0;
}

using var stream = new MemoryStream();
using var subWriter = new TRLevelWriter(stream);
subCallback(subWriter);
subWriter.Flush();

var data = stream.ToArray();
writer.Write((int)type);
writer.Write(elementCount);
writer.Write(data.Length);
writer.Write(data);

return 1;
}

private class TRXChunk
{
public TRXChunkType Type { get; set; }
public int BlockCount { get; set; }
public byte[] Data { get; set; }

public void Serialize(TRLevelWriter writer)
{
writer.Write((int)Type);
writer.Write(BlockCount);
writer.Write(Data.Length);
writer.Write(Data);
}

public static TRXChunk Read(TRLevelReader reader)
{
var chunk = new TRXChunk
{
Type = (TRXChunkType)reader.ReadInt32(),
BlockCount = reader.ReadInt32(),
};
int blockLength = reader.ReadInt32();
chunk.Data = reader.ReadBytes(blockLength);
return chunk;
}
}

private enum TRXChunkType
{
SFXData = 5,
}

private enum TRXBlockType
{
SampleInfos = 14,
}
}
5 changes: 4 additions & 1 deletion TRLevelControl/Model/TRLevelBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace TRLevelControl.Model;
using TRLevelControl.Model.TRX;

namespace TRLevelControl.Model;

public abstract class TRLevelBase
{
Expand All @@ -9,4 +11,5 @@ public abstract class TRLevelBase
public List<TRObjectTexture> ObjectTextures { get; set; }
public List<TRAnimatedTexture> AnimatedTextures { get; set; }
public abstract IEnumerable<TRMesh> DistinctMeshes { get; }
public TRXInjectionData TRXData { get; set; }
}
44 changes: 44 additions & 0 deletions TRLevelControl/Model/TRX/TRSFXData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace TRLevelControl.Model.TRX;

public class TRSFXData
{
public short ID { get; set; }
public ushort Volume { get; set; }
public ushort Chance { get; set; }
public ushort Flags { get; set; }
public List<byte[]> Data { get; set; }

public static TRSFXData Read(TRLevelReader reader)
{
var sfx = new TRSFXData
{
ID = reader.ReadInt16(),
Volume = reader.ReadUInt16(),
Chance = reader.ReadUInt16(),
Flags = reader.ReadUInt16(),
Data = [],
};

int sampleCount = (sfx.Flags & 0xFC) >> 2;
for (int i = 0; i < sampleCount; i++)
{
var length = reader.ReadInt32();
sfx.Data.Add(reader.ReadBytes(length));
}

return sfx;
}

public void Write(TRLevelWriter writer)
{
writer.Write(ID);
writer.Write(Volume);
writer.Write(Chance);
writer.Write(Flags);
Data.ForEach(wav =>
{
writer.Write(wav.Length);
writer.Write(wav);
});
}
}
6 changes: 6 additions & 0 deletions TRLevelControl/Model/TRX/TRXInjectionData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace TRLevelControl.Model.TRX;

public class TRXInjectionData
{
public List<TRSFXData> SFX { get; set; } = [];
}
3 changes: 3 additions & 0 deletions TRLevelControl/TRLevelControlBase.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics;
using TRLevelControl.IO;
using TRLevelControl.Model;

namespace TRLevelControl;
Expand All @@ -24,6 +25,7 @@ public L Read(Stream stream)
_level = CreateLevel((TRFileVersion)reader.ReadUInt32());
Initialise();
Read(reader);
_level.TRXData = TRXInjector.Read(reader);

Debug.Assert(reader.BaseStream.Position == reader.BaseStream.Length);
return _level;
Expand All @@ -41,6 +43,7 @@ public void Write(L level, Stream outputStream)
_level = level;
Initialise();
Write(writer);
TRXInjector.Write(level.TRXData, writer);
}

protected abstract L CreateLevel(TRFileVersion version);
Expand Down
44 changes: 44 additions & 0 deletions TRLevelControlTests/TRX/IOTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using TRLevelControl.Model;
using TRLevelControl.Model.TRX;

namespace TRLevelControlTests.TRX;

[TestClass]
[TestCategory("TRXInjection")]
public class IOTests : TestBase
{
[TestMethod]
public void TestSFX()
{
var level = GetTR1TestLevel();
Assert.IsNull(level.TRXData);

var sfxA = new TRSFXData
{
ID = (short)TR1SFX.NatlaDeath,
Chance = 4,
Flags = 1 << 2,
Volume = 16384,
Data = [[.. Enumerable.Range(0, 256).Select(i => (byte)i)]],
};
level.TRXData = new();
level.TRXData.SFX.Add(sfxA);
level.TRXData.SFX.Add(sfxA);

level = WriteReadTempLevel(level);

Assert.IsNotNull(level.TRXData);
Assert.HasCount(2, level.TRXData.SFX);
Assert.AreNotEqual(level.TRXData.SFX[0], level.TRXData.SFX[1]);

for (int i = 0; i < 2; i++)
{
var sfxB = level.TRXData.SFX[i];
Assert.AreEqual(sfxA.ID, sfxB.ID);
Assert.AreEqual(sfxA.Chance, sfxB.Chance);
Assert.AreEqual(sfxA.Flags, sfxB.Flags);
Assert.AreEqual(sfxA.Volume, sfxB.Volume);
CollectionAssert.AreEqual(sfxA.Data, sfxB.Data);
}
}
}
25 changes: 16 additions & 9 deletions TRRandomizerCore/Editors/TR2ClassicEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,18 +276,19 @@ protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMoni
environmentRandomizer.FinalizeEnvironment();
}

var audioRandomizer = new TR2AudioRandomizer
{
ScriptEditor = scriptEditor,
Levels = levels,
BasePath = wipDirectory,
BackupPath = backupDirectory,
SaveMonitor = monitor,
Settings = Settings
};
if (!monitor.IsCancelled && Settings.RandomizeAudio)
{
monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing audio tracks");
new TR2AudioRandomizer
{
ScriptEditor = scriptEditor,
Levels = levels,
BasePath = wipDirectory,
BackupPath = backupDirectory,
SaveMonitor = monitor,
Settings = Settings
}.Randomize(Settings.AudioSeed);
audioRandomizer.Randomize(Settings.AudioSeed);
}

if (!monitor.IsCancelled && Settings.RandomizeOutfits)
Expand Down Expand Up @@ -358,6 +359,12 @@ protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMoni
itemRandomizer.RandomizeSprites();
}

if (!monitor.IsCancelled && Settings.RandomizeAudio)
{
monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Embedding sound effects");
audioRandomizer.EmbedSamples();
}

AmendTitleAndCredits(scriptEditor, monitor);
}

Expand Down
Loading