diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e9bb44a..f72a993d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - added gun holsters to Lara's robe outfit in TR2 (#672) - fixed a key item softlock in Crash Site (#662) - fixed incorrect item and mesh positions in Home Sweet Home when mirrored (#676) +- fixed uncontrolled SFX in gym/assault course levels not being linked to the correct setting (#684) ## [V1.8.4](https://github.com/LostArtefacts/TR-Rando/compare/V1.8.3...V1.8.4) - 2024-02-12 - fixed item locking logic so that secrets that rely on specific enemies will always be obtainable (#570) diff --git a/TRRandomizerCore/Randomizers/Shared/AudioRandomizer.cs b/TRRandomizerCore/Randomizers/Shared/AudioRandomizer.cs index b033be91..90546a3a 100644 --- a/TRRandomizerCore/Randomizers/Shared/AudioRandomizer.cs +++ b/TRRandomizerCore/Randomizers/Shared/AudioRandomizer.cs @@ -3,31 +3,71 @@ using TRLevelControl; using TRLevelControl.Model; using TRRandomizerCore.Editors; +using TRRandomizerCore.Helpers; using TRRandomizerCore.SFX; namespace TRRandomizerCore.Randomizers; -public class AudioRandomizer +public abstract class AudioRandomizer { private readonly IReadOnlyDictionary> _tracks; private readonly Dictionary _trackMap; + private List _uncontrolledLevels; + + public Random Generator { get; set; } + public RandomizerSettings Settings { get; set; } + public List Categories { get; private set; } public AudioRandomizer(IReadOnlyDictionary> tracks) { _tracks = tracks; - _trackMap = new Dictionary(); + _trackMap = new(); + } + + public void Initialise(IEnumerable levelNames, string backupPath) + { + Categories = GetSFXCategories(); + ChooseUncontrolledLevels(new(levelNames), GetAssaultName()); + LoadData(backupPath); } - public void ResetFloorMap() + protected abstract string GetAssaultName(); + protected abstract void LoadData(string backupPath); + + public void ChooseUncontrolledLevels(List levels, string assaultCourse) + { + HashSet exlusions = new() { assaultCourse }; + _uncontrolledLevels = levels.RandomSelection(Generator, (int)Settings.UncontrolledSFXCount, exclusions: exlusions); + if (Settings.UncontrolledSFXAssaultCourse) + { + _uncontrolledLevels.Add(assaultCourse); + } + } + + public bool IsUncontrolledLevel(string level) + => _uncontrolledLevels.Any(l => string.Equals(level, l, StringComparison.InvariantCultureIgnoreCase)); + + public void RandomizeFloorTracks(List rooms, FDControl floorData) + where R : TRRoom { + if (!Settings.ChangeTriggerTracks) + { + return; + } + + // TRRoomFlag.Unused2 is used in mods elsewhere to indicate that music tracks are locked. _trackMap.Clear(); + foreach (TRRoom room in rooms.Where(r => !r.Flags.HasFlag(TRRoomFlag.Unused2))) + { + RandomizeFloorTracks(room, floorData); + } } - public void RandomizeFloorTracks(List sectors, FDControl floorData, Random generator, Func positionAction) + protected void RandomizeFloorTracks(TRRoom room, FDControl floorData) { - for (int i = 0; i < sectors.Count; i++) + for (int i = 0; i < room.Sectors.Count; i++) { - TRRoomSector sector = sectors[i]; + TRRoomSector sector = room.Sectors[i]; FDActionItem trackItem = null; if (sector.FDIndex > 0) { @@ -45,7 +85,11 @@ public void RandomizeFloorTracks(List sectors, FDControl floorData // Get this sector's midpoint in world coordinates. Store each immediately // neighbouring tile to use the same track as this one, regardless of room. - Vector2 position = positionAction.Invoke(i); + Vector2 position = new + ( + TRConsts.Step2 + room.Info.X + i / room.NumZSectors * TRConsts.Step4, + TRConsts.Step2 + room.Info.Z + i % room.NumZSectors * TRConsts.Step4 + ); int x = (int)position.X; int z = (int)position.Y; @@ -53,7 +97,7 @@ public void RandomizeFloorTracks(List sectors, FDControl floorData { TRAudioCategory category = FindTrackCategory((ushort)trackItem.Parameter); List tracks = _tracks[category]; - _trackMap[position] = tracks[generator.Next(0, tracks.Count)].ID; + _trackMap[position] = tracks[Generator.Next(0, tracks.Count)].ID; } for (int xNorm = -1; xNorm < 2; xNorm++) @@ -74,6 +118,60 @@ public void RandomizeFloorTracks(List sectors, FDControl floorData } } + public void RandomizeSecretTracks(FDControl floorData, ushort defaultTrackID) + { + int numSecrets = floorData.GetActionItems(FDTrigAction.SecretFound) + .Select(a => a.Parameter) + .Distinct().Count(); + if (numSecrets == 0 || !Settings.SeparateSecretTracks) + { + return; + } + + List secretTracks = GetTracks(TRAudioCategory.Secret); + + for (int i = 0; i < numSecrets; i++) + { + TRAudioTrack secretTrack = secretTracks[Generator.Next(0, secretTracks.Count)]; + if (secretTrack.ID == defaultTrackID) + { + continue; + } + + FDActionItem musicAction = new() + { + Action = FDTrigAction.PlaySoundtrack, + Parameter = (short)secretTrack.ID + }; + + List triggers = floorData.GetSecretTriggers(i); + foreach (FDTriggerEntry trigger in triggers) + { + FDActionItem currentMusicAction = trigger.Actions.Find(a => a.Action == FDTrigAction.PlaySoundtrack); + if (currentMusicAction == null) + { + trigger.Actions.Add(musicAction); + } + } + } + } + + public void RandomizePitch(IEnumerable> effects) + where T : Enum + { + if (!Settings.RandomizeWibble) + { + return; + } + + // The engine does the actual randomization, we just tell it that every + // sound effect should be included. + foreach (TRSoundEffect effect in effects) + { + effect.RandomizePitch = true; + } + } + public TRAudioCategory FindTrackCategory(ushort trackID) { foreach (TRAudioCategory category in _tracks.Keys) @@ -95,10 +193,10 @@ public List GetTracks(TRAudioCategory category) return _tracks[category]; } - public static List GetSFXCategories(RandomizerSettings settings) + private List GetSFXCategories() { List sfxCategories = new(); - if (settings.ChangeWeaponSFX) + if (Settings.ChangeWeaponSFX) { // Pistols, Autos etc sfxCategories.Add(TRSFXGeneralCategory.StandardWeaponFiring); @@ -108,7 +206,7 @@ public static List GetSFXCategories(RandomizerSettings set sfxCategories.Add(TRSFXGeneralCategory.Ricochet); } - if (settings.ChangeCrashSFX) + if (Settings.ChangeCrashSFX) { // Grenades, 40F crash, dragon explosion sfxCategories.Add(TRSFXGeneralCategory.Explosion); @@ -118,7 +216,7 @@ public static List GetSFXCategories(RandomizerSettings set sfxCategories.Add(TRSFXGeneralCategory.Breaking); } - if (settings.ChangeEnemySFX) + if (Settings.ChangeEnemySFX) { // General death noises sfxCategories.Add(TRSFXGeneralCategory.Death); @@ -140,7 +238,7 @@ public static List GetSFXCategories(RandomizerSettings set sfxCategories.Add(TRSFXGeneralCategory.Flying); } - if (settings.ChangeDoorSFX) + if (Settings.ChangeDoorSFX) { // Doors that share opening/closing sounds sfxCategories.Add(TRSFXGeneralCategory.GeneralDoor); diff --git a/TRRandomizerCore/Randomizers/TR1/Classic/TR1AudioRandomizer.cs b/TRRandomizerCore/Randomizers/TR1/Classic/TR1AudioRandomizer.cs index db3bdac8..23d2f1fb 100644 --- a/TRRandomizerCore/Randomizers/TR1/Classic/TR1AudioRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR1/Classic/TR1AudioRandomizer.cs @@ -1,10 +1,5 @@ -using Newtonsoft.Json; -using System.Numerics; -using TRGE.Core; -using TRLevelControl; -using TRLevelControl.Helpers; +using TRGE.Core; using TRLevelControl.Model; -using TRRandomizerCore.Helpers; using TRRandomizerCore.Levels; using TRRandomizerCore.SFX; @@ -14,133 +9,53 @@ public class TR1AudioRandomizer : BaseTR1Randomizer { private static readonly List _speechTracks = Enumerable.Range(51, 6).ToList(); private static readonly TR1SFX _sfxFirstSpeechID = TR1SFX.BaldySpeech; - private static readonly TR1SFX _sfxUziID = TR1SFX.LaraUziFire; + + private const double _psUziChance = 0.4; - private AudioRandomizer _audioRandomizer; - - private List _soundEffects; - private TR1SFXDefinition _psUziDefinition; - private List _sfxCategories, _persistentCategories; - private List _uncontrolledLevels; + private TR1AudioAllocator _allocator; public override void Randomize(int seed) { - _generator = new Random(seed); - - LoadAudioData(); - ChooseUncontrolledLevels(); + _generator = new(seed); + _allocator = new(ScriptEditor.AudioProvider.GetCategorisedTracks()) + { + Generator = _generator, + Settings = Settings, + }; + _allocator.Initialise(Levels.Select(l => l.LevelFileBaseName), BackupPath); foreach (TR1ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeMusicTriggers(_levelInstance); - + _allocator.RandomizeMusicTriggers(_levelInstance.Data); RandomizeSoundEffects(_levelInstance); - - ImportSpeechSFX(_levelInstance); - - RandomizeWibble(_levelInstance); + ImportSpeechSFX(_levelInstance.Data); + _allocator.RandomizePitch(_levelInstance.Data.SoundEffects.Values); SaveLevelInstance(); - if (!TriggerProgress()) { break; } } - } - - private void LoadAudioData() - { - // Get the track data from audio_tracks.json. Loaded from TRGE as it sets the ambient tracks initially. - _audioRandomizer = new AudioRandomizer(ScriptEditor.AudioProvider.GetCategorisedTracks()); - - // Decide which sound effect categories we want to randomize. - _sfxCategories = AudioRandomizer.GetSFXCategories(Settings); - - // SFX in these categories can potentially remain as they are - _persistentCategories = new List - { - TRSFXGeneralCategory.StandardWeaponFiring, - TRSFXGeneralCategory.Ricochet, - TRSFXGeneralCategory.Flying, - TRSFXGeneralCategory.Explosion - }; - - _soundEffects = JsonConvert.DeserializeObject>(ReadResource(@"TR1\Audio\sfx.json")); - - // We don't want to store all SFX WAV data in JSON, so instead we reference the source level - // and extract the details from there using the same format for model transport. - Dictionary levels = new(); - TR1LevelControl reader = new(); - foreach (TR1SFXDefinition definition in _soundEffects) - { - if (!levels.ContainsKey(definition.SourceLevel)) - { - levels[definition.SourceLevel] = reader.Read(Path.Combine(BackupPath, definition.SourceLevel)); - } - - TR1Level level = levels[definition.SourceLevel]; - definition.SoundEffect = level.SoundEffects[definition.InternalIndex]; - } - - // PS uzis need some manual setup. Make a copy of the standard uzi definition - // then replace the sound data from the external wav file. - TR1Level caves = levels[TR1LevelNames.CAVES]; - _psUziDefinition = new TR1SFXDefinition - { - SoundEffect = caves.SoundEffects[_sfxUziID] - }; - _psUziDefinition.SoundEffect.Samples = new() { File.ReadAllBytes(GetResourcePath(@"TR1\Audio\ps_uzis.wav")) }; - } - - private void ChooseUncontrolledLevels() - { - TR1ScriptedLevel assaultCourse = Levels.Find(l => l.Is(TR1LevelNames.ASSAULT)); - ISet exlusions = new HashSet { assaultCourse }; - _uncontrolledLevels = Levels.RandomSelection(_generator, (int)Settings.UncontrolledSFXCount, exclusions: exlusions); - if (Settings.AssaultCourseWireframe) - { - _uncontrolledLevels.Add(assaultCourse); - } - } - - public bool IsUncontrolledLevel(TR1ScriptedLevel level) - { - return _uncontrolledLevels.Contains(level); - } - - private void RandomizeMusicTriggers(TR1CombinedLevel level) - { - if (Settings.ChangeTriggerTracks) + if (Settings.RandomizeWibble) { - _audioRandomizer.ResetFloorMap(); - foreach (TR1Room room in level.Data.Rooms.Where(r => !r.Flags.HasFlag(TRRoomFlag.Unused2))) - { - _audioRandomizer.RandomizeFloorTracks(room.Sectors, level.Data.FloorData, _generator, sectorIndex => - { - // Get the midpoint of the tile in world coordinates - return new Vector2 - ( - TRConsts.Step2 + room.Info.X + sectorIndex / room.NumZSectors * TRConsts.Step4, - TRConsts.Step2 + room.Info.Z + sectorIndex % room.NumZSectors * TRConsts.Step4 - ); - }); - } + (ScriptEditor as TR1ScriptEditor).EnablePitchedSounds = true; + ScriptEditor.SaveScript(); } } private void RandomizeSoundEffects(TR1CombinedLevel level) { - if (_sfxCategories.Count == 0) + List soundEffects = _allocator.GetDefinitions(); + if (_allocator.Categories.Count == 0) { - // We haven't selected any SFX categories to change. return; } - if (IsUncontrolledLevel(level.Script)) + if (_allocator.IsUncontrolledLevel(level.Name)) { HashSet usedSamples = new(); @@ -153,7 +68,7 @@ private void RandomizeSoundEffects(TR1CombinedLevel level) string id; do { - TR1SFXDefinition definition = _soundEffects[_generator.Next(0, _soundEffects.Count)]; + TR1SFXDefinition definition = soundEffects[_generator.Next(0, soundEffects.Count)]; int sampleIndex = _generator.Next(0, definition.SoundEffect.Samples.Count); sample = definition.SoundEffect.Samples[sampleIndex]; @@ -167,11 +82,14 @@ private void RandomizeSoundEffects(TR1CombinedLevel level) } else { + // Run through the SoundMap for this level and get the SFX definition for each one. + // Choose a new sound effect provided the definition is in a category we want to change. + // Lara's SFX are not changed by default. foreach (TR1SFX internalIndex in Enum.GetValues()) { - TR1SFXDefinition definition = _soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); + TR1SFXDefinition definition = soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); if (!level.Data.SoundEffects.ContainsKey(internalIndex) || definition == null - || definition.Creature == TRSFXCreatureCategory.Lara || !_sfxCategories.Contains(definition.PrimaryCategory)) + || definition.Creature == TRSFXCreatureCategory.Lara || !_allocator.Categories.Contains(definition.PrimaryCategory)) { continue; } @@ -184,7 +102,7 @@ private void RandomizeSoundEffects(TR1CombinedLevel level) pred = sfx => { return sfx.Categories.Contains(definition.PrimaryCategory) && - (sfx != definition || _persistentCategories.Contains(definition.PrimaryCategory)) && + (sfx != definition || _allocator.IsPersistent(definition.PrimaryCategory)) && ( sfx.Creature == definition.Creature || (sfx.Creature == TRSFXCreatureCategory.Lara && definition.Creature == TRSFXCreatureCategory.Human) @@ -193,19 +111,18 @@ private void RandomizeSoundEffects(TR1CombinedLevel level) } else { - pred = sfx => sfx.Categories.Contains(definition.PrimaryCategory) && (sfx != definition || _persistentCategories.Contains(definition.PrimaryCategory)); + pred = sfx => sfx.Categories.Contains(definition.PrimaryCategory) && (sfx != definition || _allocator.IsPersistent(definition.PrimaryCategory)); } List otherDefinitions; - if (internalIndex == _sfxUziID && _generator.NextDouble() < 0.4) + if (internalIndex == TR1SFX.LaraUziFire && _generator.NextDouble() < _psUziChance) { // 2/5 chance of PS uzis replacing original uzis, but they won't be used for anything else - otherDefinitions = new() { _psUziDefinition }; + otherDefinitions = new() { _allocator.GetPSUziDefinition() }; } else { - // Try to find definitions that match - otherDefinitions = _soundEffects.FindAll(pred); + otherDefinitions = soundEffects.FindAll(pred); } if (otherDefinitions.Count > 0) @@ -228,18 +145,16 @@ private void RandomizeSoundEffects(TR1CombinedLevel level) } } - private void ImportSpeechSFX(TR1CombinedLevel level) + private void ImportSpeechSFX(TR1Level level) { if (!(ScriptEditor as TR1ScriptEditor).FixSpeechesKillingMusic) { return; } - // TR1X can play enemy speeches as SFX to avoid killing the current - // track, so ensure that the required data is in the level if any - // of these are used on the floor. - - List usedSpeechTracks = level.Data.FloorData.GetActionItems(FDTrigAction.PlaySoundtrack) + // TR1X can play enemy speeches as SFX to avoid killing the current track, so ensure that + // the required data is in the level if any of these are used on the floor. + List usedSpeechTracks = level.FloorData.GetActionItems(FDTrigAction.PlaySoundtrack) .Select(action => (ushort)action.Parameter) .Distinct() .Where(trackID => _speechTracks.Contains(trackID)) @@ -250,33 +165,18 @@ private void ImportSpeechSFX(TR1CombinedLevel level) return; } + List soundEffects = _allocator.GetDefinitions(); foreach (ushort trackID in usedSpeechTracks) { TR1SFX sfxID = (TR1SFX)((int)_sfxFirstSpeechID + trackID - _speechTracks.First()); TR1SFXDefinition definition; - if (level.Data.SoundEffects.ContainsKey(sfxID) - || (definition = _soundEffects.Find(sfx => sfx.InternalIndex == sfxID)) == null) + if (level.SoundEffects.ContainsKey(sfxID) + || (definition = soundEffects.Find(sfx => sfx.InternalIndex == sfxID)) == null) { continue; } - level.Data.SoundEffects[sfxID] = definition.SoundEffect; - } - } - - private void RandomizeWibble(TR1CombinedLevel level) - { - if (Settings.RandomizeWibble) - { - // The engine does the actual randomization, we just tell it that every - // sound effect should be included. - foreach (var (_, effect) in level.Data.SoundEffects) - { - effect.RandomizePitch = true; - } - - (ScriptEditor as TR1ScriptEditor).EnablePitchedSounds = true; - ScriptEditor.SaveScript(); + level.SoundEffects[sfxID] = definition.SoundEffect; } } } diff --git a/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RAudioRandomizer.cs b/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RAudioRandomizer.cs index 0e2d1fc2..afdf2e0e 100644 --- a/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RAudioRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR1/Remastered/TR1RAudioRandomizer.cs @@ -1,29 +1,31 @@ -using System.Numerics; -using TRGE.Core; -using TRLevelControl; +using TRGE.Core; using TRLevelControl.Model; using TRRandomizerCore.Levels; +using TRRandomizerCore.SFX; namespace TRRandomizerCore.Randomizers; public class TR1RAudioRandomizer : BaseTR1RRandomizer { - private const int _defaultSecretTrack = 13; - - private AudioRandomizer _audioRandomizer; + private TR1AudioAllocator _allocator; public override void Randomize(int seed) { _generator = new(seed); - - LoadAudioData(); + _allocator = new(ScriptEditor.AudioProvider.GetCategorisedTracks()) + { + Generator = _generator, + Settings = Settings, + }; + _allocator.Initialise(Levels.Select(l => l.LevelFileBaseName), BackupPath); foreach (TRRScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeMusicTriggers(_levelInstance); - RandomizeWibble(_levelInstance); + _allocator.RandomizeMusicTriggers(_levelInstance.Data); + RandomizeSoundEffects(_levelInstance); + _allocator.RandomizePitch(_levelInstance.Data.SoundEffects.Values); SaveLevelInstance(); if (!TriggerProgress()) @@ -33,78 +35,83 @@ public override void Randomize(int seed) } } - private void LoadAudioData() + private void RandomizeSoundEffects(TR1RCombinedLevel level) { - _audioRandomizer = new(ScriptEditor.AudioProvider.GetCategorisedTracks()); - } - - private void RandomizeMusicTriggers(TR1RCombinedLevel level) - { - if (Settings.ChangeTriggerTracks) + if (_allocator.Categories.Count == 0) { - RandomizeFloorTracks(level.Data); + return; } - if (Settings.SeparateSecretTracks) + // TR1R has hardcoded sample indices (into MAIN.SFX) so we use a different approach compared to OG, by + // instead changing any animation commands and sound sources to point to different IDs. This does + // however mean hardcoded SFX like Lara's guns remain unchanged. + void ChangeCommands(IEnumerable models) { - RandomizeSecretTracks(level); + IEnumerable commands = models + .SelectMany(m => m.Animations.SelectMany(a => a.Commands.Where(c => c is TRSFXCommand))) + .Cast(); + foreach (TRSFXCommand command in commands) + { + TR1SFXDefinition definition = SelectSFXReplacement(level, (TR1SFX)command.SoundID); + if (definition != null) + { + command.SoundID = (short)definition.InternalIndex; + level.Data.SoundEffects[definition.InternalIndex] = definition.SoundEffect.Clone(); + } + } } - } - private void RandomizeFloorTracks(TR1Level level) - { - _audioRandomizer.ResetFloorMap(); - foreach (TR1Room room in level.Rooms.Where(r => !r.Flags.HasFlag(TRRoomFlag.Unused2))) + ChangeCommands(level.Data.Models.Values); + ChangeCommands(level.PDPData.Values); + + foreach (TRSoundSource soundSource in level.Data.SoundSources) { - _audioRandomizer.RandomizeFloorTracks(room.Sectors, level.FloorData, _generator, sectorIndex => + TR1SFXDefinition definition = SelectSFXReplacement(level, soundSource.ID); + if (definition != null) { - return new Vector2 - ( - TRConsts.Step2 + room.Info.X + sectorIndex / room.NumZSectors * TRConsts.Step4, - TRConsts.Step2 + room.Info.Z + sectorIndex % room.NumZSectors * TRConsts.Step4 - ); - }); + soundSource.ID = definition.InternalIndex; + level.Data.SoundEffects[definition.InternalIndex] = definition.SoundEffect.Clone(); + } } } - private void RandomizeSecretTracks(TR1RCombinedLevel level) + private TR1SFXDefinition SelectSFXReplacement(TR1RCombinedLevel level, TR1SFX currentSFX) { - List secretTracks = _audioRandomizer.GetTracks(TRAudioCategory.Secret); + List soundEffects = _allocator.GetDefinitions(); - for (int i = 0; i < level.Script.NumSecrets; i++) + if (_allocator.IsUncontrolledLevel(level.Name)) { - TRAudioTrack secretTrack = secretTracks[_generator.Next(0, secretTracks.Count)]; - if (secretTrack.ID == _defaultSecretTrack) - { - continue; - } - - FDActionItem musicAction = new() - { - Action = FDTrigAction.PlaySoundtrack, - Parameter = (short)secretTrack.ID - }; + return soundEffects[_generator.Next(0, soundEffects.Count)]; + } - List triggers = level.Data.FloorData.GetSecretTriggers(i); - foreach (FDTriggerEntry trigger in triggers) - { - FDActionItem currentMusicAction = trigger.Actions.Find(a => a.Action == FDTrigAction.PlaySoundtrack); - if (currentMusicAction == null) - { - trigger.Actions.Add(musicAction); - } - } + TR1SFXDefinition definition = soundEffects.Find(sfx => sfx.InternalIndex == currentSFX); + if (definition == null + || definition.Creature == TRSFXCreatureCategory.Lara || !_allocator.Categories.Contains(definition.PrimaryCategory)) + { + return null; } - } - private void RandomizeWibble(TR1RCombinedLevel level) - { - if (Settings.RandomizeWibble) + Predicate pred; + if (Settings.LinkCreatureSFX && definition.Creature > TRSFXCreatureCategory.Lara) { - foreach (var (_, effect) in level.Data.SoundEffects) + pred = sfx => { - effect.RandomizePitch = true; - } + return sfx.Categories.Contains(definition.PrimaryCategory) && + (sfx != definition || _allocator.IsPersistent(definition.PrimaryCategory)) && + ( + sfx.Creature == definition.Creature || + (sfx.Creature == TRSFXCreatureCategory.Lara && definition.Creature == TRSFXCreatureCategory.Human) + ); + }; + } + else + { + pred = sfx => sfx.Categories.Contains(definition.PrimaryCategory) && (sfx != definition || _allocator.IsPersistent(definition.PrimaryCategory)); } + + List otherDefinitions = soundEffects.FindAll(pred); + return otherDefinitions.Any() + ? otherDefinitions[_generator.Next(0, otherDefinitions.Count)] + : null; } } diff --git a/TRRandomizerCore/Randomizers/TR1/Shared/TR1AudioAllocator.cs b/TRRandomizerCore/Randomizers/TR1/Shared/TR1AudioAllocator.cs new file mode 100644 index 00000000..ed2f5f3b --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR1/Shared/TR1AudioAllocator.cs @@ -0,0 +1,79 @@ +using Newtonsoft.Json; +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRRandomizerCore.SFX; + +namespace TRRandomizerCore.Randomizers; + +public class TR1AudioAllocator : AudioRandomizer +{ + private const int _defaultSecretTrack = 13; + + private List _soundEffects; + private TR1SFXDefinition _psUziDefinition; + private List _persistentCategories; + + public TR1AudioAllocator(IReadOnlyDictionary> tracks) + : base(tracks) { } + + public List GetDefinitions() + => _soundEffects; + + public bool IsPersistent(TRSFXGeneralCategory category) + => _persistentCategories.Contains(category); + + public TR1SFXDefinition GetPSUziDefinition() + => _psUziDefinition; + + protected override string GetAssaultName() + => TR1LevelNames.ASSAULT; + + protected override void LoadData(string backupPath) + { + // SFX in these categories can potentially remain as they are + _persistentCategories = new() + { + TRSFXGeneralCategory.StandardWeaponFiring, + TRSFXGeneralCategory.Ricochet, + TRSFXGeneralCategory.Flying, + TRSFXGeneralCategory.Explosion, + }; + + _soundEffects = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR1\Audio\sfx.json")); + + // We don't want to store all SFX WAV data in JSON, so instead we reference the source level + // and extract the details from there using the same format for model transport. + Dictionary levels = new(); + TR1LevelControl reader = new(); + foreach (TR1SFXDefinition definition in _soundEffects) + { + if (!levels.ContainsKey(definition.SourceLevel)) + { + levels[definition.SourceLevel] = reader.Read(Path.Combine(backupPath, definition.SourceLevel)); + } + + TR1Level level = levels[definition.SourceLevel]; + definition.SoundEffect = level.SoundEffects[definition.InternalIndex]; + } + + // PS uzis need some manual setup. Make a copy of the standard uzi definition + // then replace the sound data from the external wav file. + TR1Level caves = levels[TR1LevelNames.CAVES]; + _psUziDefinition = new TR1SFXDefinition + { + SoundEffect = caves.SoundEffects[TR1SFX.LaraUziFire] + }; + _psUziDefinition.SoundEffect.Samples = new() { File.ReadAllBytes(@"Resources\TR1\Audio\ps_uzis.wav") }; + } + + public void RandomizeMusicTriggers(TR1Level level) + { + RandomizeFloorTracks(level.Rooms, level.FloorData); + if (!Settings.RandomizeSecrets) + { + RandomizeSecretTracks(level.FloorData, _defaultSecretTrack); + } + } +} diff --git a/TRRandomizerCore/Randomizers/TR2/Classic/TR2AudioRandomizer.cs b/TRRandomizerCore/Randomizers/TR2/Classic/TR2AudioRandomizer.cs index fa3713f3..625867b6 100644 --- a/TRRandomizerCore/Randomizers/TR2/Classic/TR2AudioRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR2/Classic/TR2AudioRandomizer.cs @@ -1,12 +1,4 @@ -using Newtonsoft.Json; -using System.Numerics; -using TRGE.Core; -using TRLevelControl; -using TRLevelControl.Helpers; -using TRLevelControl.Model; -using TRRandomizerCore.Helpers; -using TRRandomizerCore.Levels; -using TRRandomizerCore.SFX; +using TRGE.Core; namespace TRRandomizerCore.Randomizers; @@ -14,269 +6,28 @@ public class TR2AudioRandomizer : BaseTR2Randomizer { private const int _numSamples = 408; // Number of entries in MAIN.SFX - private AudioRandomizer _audioRandomizer; - - private List _soundEffects; - private List _sfxCategories; - private List _uncontrolledLevels; - public override void Randomize(int seed) { - _generator = new Random(seed); - - LoadAudioData(); - ChooseUncontrolledLevels(); + TR2AudioAllocator allocator = new(ScriptEditor.AudioProvider.GetCategorisedTracks(), _numSamples) + { + Generator = new(seed), + Settings = Settings, + }; + allocator.Initialise(Levels.Select(l => l.LevelFileBaseName), BackupPath); foreach (TR2ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeMusicTriggers(_levelInstance); - - RandomizeSoundEffects(_levelInstance); - - RandomizeWibble(_levelInstance); + allocator.RandomizeMusicTriggers(_levelInstance.Data); + allocator.RandomizeSoundEffects(_levelInstance.Name, _levelInstance.Data); + allocator.RandomizePitch(_levelInstance.Data.SoundEffects.Values); SaveLevelInstance(); - if (!TriggerProgress()) { break; } } } - - private void ChooseUncontrolledLevels() - { - TR2ScriptedLevel assaultCourse = Levels.Find(l => l.Is(TR2LevelNames.ASSAULT)); - ISet exlusions = new HashSet { assaultCourse }; - - _uncontrolledLevels = Levels.RandomSelection(_generator, (int)Settings.UncontrolledSFXCount, exclusions: exlusions); - if (Settings.AssaultCourseWireframe) - { - _uncontrolledLevels.Add(assaultCourse); - } - } - - public bool IsUncontrolledLevel(TR2ScriptedLevel level) - { - return _uncontrolledLevels.Contains(level); - } - - private void RandomizeMusicTriggers(TR2CombinedLevel level) - { - if (Settings.ChangeTriggerTracks) - { - RandomizeFloorTracks(level.Data); - } - - if (Settings.SeparateSecretTracks) - { - RandomizeSecretTracks(level.Data); - } - } - - private void RandomizeFloorTracks(TR2Level level) - { - _audioRandomizer.ResetFloorMap(); - foreach (TR2Room room in level.Rooms) - { - _audioRandomizer.RandomizeFloorTracks(room.Sectors, level.FloorData, _generator, sectorIndex => - { - // Get the midpoint of the tile in world coordinates - return new Vector2 - ( - TRConsts.Step2 + room.Info.X + sectorIndex / room.NumZSectors * TRConsts.Step4, - TRConsts.Step2 + room.Info.Z + sectorIndex % room.NumZSectors * TRConsts.Step4 - ); - }); - } - } - - private void RandomizeSecretTracks(TR2Level level) - { - // Generate new triggers for secrets to allow different sounds for each one - List secretTracks = _audioRandomizer.GetTracks(TRAudioCategory.Secret); - Dictionary secrets = GetSecretItems(level); - foreach (int entityIndex in secrets.Keys) - { - TR2Entity secret = secrets[entityIndex]; - TRRoomSector sector = level.GetRoomSector(secret); - if (sector.FDIndex == 0) - { - // The secret is positioned on a tile that currently has no FD, so create it - level.FloorData.CreateFloorData(sector); - } - - List entries = level.FloorData[sector.FDIndex]; - FDTriggerEntry existingTriggerEntry = entries.Find(e => e is FDTriggerEntry) as FDTriggerEntry; - bool existingEntityPickup = false; - if (existingTriggerEntry != null) - { - if (existingTriggerEntry.TrigType == FDTrigType.Pickup && existingTriggerEntry.Actions[0].Parameter == entityIndex) - { - // GW gold secret (default location) already has a pickup trigger to spawn the - // TRex (or whatever enemy) so we'll just append to that item list here - existingEntityPickup = true; - } - else - { - // There is already a non-pickup trigger for this sector so while nothing is wrong with - // adding a pickup trigger, the game ignores it. So in this instance, the sound that - // plays will just be whatever is set in the script. - continue; - } - } - - // Generate a new music action - FDActionItem musicAction = new() - { - Action = FDTrigAction.PlaySoundtrack, - Parameter = (short)secretTracks[_generator.Next(0, secretTracks.Count)].ID - }; - - // For GW default gold, just append it - if (existingEntityPickup) - { - existingTriggerEntry.Actions.Add(musicAction); - } - else - { - entries.Add(new FDTriggerEntry - { - TrigType = FDTrigType.Pickup, - Actions = new() - { - new() { Parameter = (short)entityIndex }, - musicAction - } - }); - } - } - } - - private static Dictionary GetSecretItems(TR2Level level) - { - Dictionary entities = new(); - for (int i = 0; i < level.Entities.Count; i++) - { - if (TR2TypeUtilities.IsSecretType(level.Entities[i].TypeID)) - { - entities[i] = level.Entities[i]; - } - } - - return entities; - } - - private void LoadAudioData() - { - // Get the track data from audio_tracks.json. Loaded from TRGE as it sets the ambient tracks initially. - _audioRandomizer = new AudioRandomizer(ScriptEditor.AudioProvider.GetCategorisedTracks()); - - // Decide which sound effect categories we want to randomize. - _sfxCategories = AudioRandomizer.GetSFXCategories(Settings); - - // Only load the SFX if we are changing at least one category - if (_sfxCategories.Count > 0) - { - _soundEffects = JsonConvert.DeserializeObject>(ReadResource(@"TR2\Audio\sfx.json")); - - Dictionary levels = new(); - TR2LevelControl reader = new(); - foreach (TR2SFXDefinition definition in _soundEffects) - { - if (!levels.ContainsKey(definition.SourceLevel)) - { - levels[definition.SourceLevel] = reader.Read(Path.Combine(BackupPath, definition.SourceLevel)); - } - - TR2Level level = levels[definition.SourceLevel]; - definition.SoundEffect = level.SoundEffects[definition.InternalIndex]; - } - } - } - - private void RandomizeSoundEffects(TR2CombinedLevel level) - { - if (_sfxCategories.Count == 0) - { - // We haven't selected any SFX categories to change. - return; - } - - if (IsUncontrolledLevel(level.Script)) - { - // Choose a random but unique pointer into MAIN.SFX for each sample. - HashSet indices = new(); - foreach (TR2SoundEffect effect in level.Data.SoundEffects.Values) - { - do - { - effect.SampleID = (uint)_generator.Next(0, _numSamples + 1 - Math.Max(effect.SampleCount, 1)); - } - while (!indices.Add(effect.SampleID)); - } - } - else - { - // Run through the SoundMap for this level and get the SFX definition for each one. - // Choose a new sound effect provided the definition is in a category we want to change. - // Lara's SFX are not changed by default. - foreach (TR2SFX internalIndex in Enum.GetValues()) - { - TR2SFXDefinition definition = _soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); - if (!level.Data.SoundEffects.ContainsKey(internalIndex) || definition == null - || definition.Creature == TRSFXCreatureCategory.Lara || !_sfxCategories.Contains(definition.PrimaryCategory)) - { - continue; - } - - // The following allows choosing to keep humans making human noises, and animals animal noises. - // Other humans can use Lara's SFX. - Predicate pred; - if (Settings.LinkCreatureSFX && definition.Creature > TRSFXCreatureCategory.Lara) - { - pred = sfx => - { - return sfx.Categories.Contains(definition.PrimaryCategory) && - sfx != definition && - ( - sfx.Creature == definition.Creature || - (sfx.Creature == TRSFXCreatureCategory.Lara && definition.Creature == TRSFXCreatureCategory.Human) - ); - }; - } - else - { - pred = sfx => sfx.Categories.Contains(definition.PrimaryCategory) && sfx != definition; - } - - // Try to find definitions that match - List otherDefinitions = _soundEffects.FindAll(pred); - if (otherDefinitions.Count > 0) - { - // Pick a new definition and try to import it into the level. This should only fail if - // the JSON is misconfigured e.g. missing sample indices. In that case, we just leave - // the current sound effect as-is. - TR2SFXDefinition nextDefinition = otherDefinitions[_generator.Next(0, otherDefinitions.Count)]; - if (nextDefinition != definition) - { - level.Data.SoundEffects[internalIndex] = nextDefinition.SoundEffect; - } - } - } - } - } - - private void RandomizeWibble(TR2CombinedLevel level) - { - if (Settings.RandomizeWibble) - { - foreach (var (_, effect) in level.Data.SoundEffects) - { - effect.RandomizePitch = true; - } - } - } } diff --git a/TRRandomizerCore/Randomizers/TR2/Remastered/TR2RAudioRandomizer.cs b/TRRandomizerCore/Randomizers/TR2/Remastered/TR2RAudioRandomizer.cs index 508f24ca..90123244 100644 --- a/TRRandomizerCore/Randomizers/TR2/Remastered/TR2RAudioRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR2/Remastered/TR2RAudioRandomizer.cs @@ -1,12 +1,4 @@ -using Newtonsoft.Json; -using System.Numerics; -using TRGE.Core; -using TRLevelControl; -using TRLevelControl.Helpers; -using TRLevelControl.Model; -using TRRandomizerCore.Helpers; -using TRRandomizerCore.Levels; -using TRRandomizerCore.SFX; +using TRGE.Core; namespace TRRandomizerCore.Randomizers; @@ -14,26 +6,22 @@ public class TR2RAudioRandomizer : BaseTR2RRandomizer { private const int _numSamples = 412; - private AudioRandomizer _audioRandomizer; - - private List _soundEffects; - private List _sfxCategories; - private List _uncontrolledLevels; - public override void Randomize(int seed) { - _generator = new Random(seed); - - LoadAudioData(); - ChooseUncontrolledLevels(); + TR2AudioAllocator allocator = new(ScriptEditor.AudioProvider.GetCategorisedTracks(), _numSamples) + { + Generator = new(seed), + Settings = Settings, + }; + allocator.Initialise(Levels.Select(l => l.LevelFileBaseName), BackupPath); foreach (TRRScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeMusicTriggers(_levelInstance); - RandomizeSoundEffects(_levelInstance); - RandomizeWibble(_levelInstance); + allocator.RandomizeMusicTriggers(_levelInstance.Data); + allocator.RandomizeSoundEffects(_levelInstance.Name, _levelInstance.Data); + allocator.RandomizePitch(_levelInstance.Data.SoundEffects.Values); SaveLevelInstance(); if (!TriggerProgress()) @@ -42,212 +30,4 @@ public override void Randomize(int seed) } } } - - private void ChooseUncontrolledLevels() - { - TRRScriptedLevel assaultCourse = Levels.Find(l => l.Is(TR2LevelNames.ASSAULT)); - HashSet exlusions = new() { assaultCourse }; - - _uncontrolledLevels = Levels.RandomSelection(_generator, (int)Settings.UncontrolledSFXCount, exclusions: exlusions); - if (Settings.AssaultCourseWireframe) - { - _uncontrolledLevels.Add(assaultCourse); - } - } - - public bool IsUncontrolledLevel(TRRScriptedLevel level) - { - return _uncontrolledLevels.Contains(level); - } - - private void RandomizeMusicTriggers(TR2RCombinedLevel level) - { - if (Settings.ChangeTriggerTracks) - { - RandomizeFloorTracks(level.Data); - } - - if (Settings.SeparateSecretTracks) - { - RandomizeSecretTracks(level.Data); - } - } - - private void RandomizeFloorTracks(TR2Level level) - { - _audioRandomizer.ResetFloorMap(); - foreach (TR2Room room in level.Rooms) - { - _audioRandomizer.RandomizeFloorTracks(room.Sectors, level.FloorData, _generator, sectorIndex => - { - return new Vector2 - ( - TRConsts.Step2 + room.Info.X + sectorIndex / room.NumZSectors * TRConsts.Step4, - TRConsts.Step2 + room.Info.Z + sectorIndex % room.NumZSectors * TRConsts.Step4 - ); - }); - } - } - - private void RandomizeSecretTracks(TR2Level level) - { - List secretTracks = _audioRandomizer.GetTracks(TRAudioCategory.Secret); - Dictionary secrets = GetSecretItems(level); - foreach (int entityIndex in secrets.Keys) - { - TR2Entity secret = secrets[entityIndex]; - TRRoomSector sector = level.GetRoomSector(secret); - if (sector.FDIndex == 0) - { - level.FloorData.CreateFloorData(sector); - } - - List entries = level.FloorData[sector.FDIndex]; - FDTriggerEntry existingTriggerEntry = entries.Find(e => e is FDTriggerEntry) as FDTriggerEntry; - bool existingEntityPickup = false; - if (existingTriggerEntry != null) - { - if (existingTriggerEntry.TrigType == FDTrigType.Pickup && existingTriggerEntry.Actions[0].Parameter == entityIndex) - { - existingEntityPickup = true; - } - else - { - continue; - } - } - - FDActionItem musicAction = new() - { - Action = FDTrigAction.PlaySoundtrack, - Parameter = (short)secretTracks[_generator.Next(0, secretTracks.Count)].ID - }; - - if (existingEntityPickup) - { - existingTriggerEntry.Actions.Add(musicAction); - } - else - { - entries.Add(new FDTriggerEntry - { - TrigType = FDTrigType.Pickup, - Actions = new() - { - new() { Parameter = (short)entityIndex }, - musicAction - } - }); - } - } - } - - private static Dictionary GetSecretItems(TR2Level level) - { - Dictionary entities = new(); - for (int i = 0; i < level.Entities.Count; i++) - { - if (TR2TypeUtilities.IsSecretType(level.Entities[i].TypeID)) - { - entities[i] = level.Entities[i]; - } - } - - return entities; - } - - private void LoadAudioData() - { - _audioRandomizer = new AudioRandomizer(ScriptEditor.AudioProvider.GetCategorisedTracks()); - _sfxCategories = AudioRandomizer.GetSFXCategories(Settings); - if (_sfxCategories.Count > 0) - { - _soundEffects = JsonConvert.DeserializeObject>(ReadResource(@"TR2\Audio\sfx.json")); - - Dictionary levels = new(); - TR2LevelControl reader = new(); - foreach (TR2SFXDefinition definition in _soundEffects) - { - if (!levels.ContainsKey(definition.SourceLevel)) - { - levels[definition.SourceLevel] = reader.Read(Path.Combine(BackupPath, definition.SourceLevel)); - } - - TR2Level level = levels[definition.SourceLevel]; - definition.SoundEffect = level.SoundEffects[definition.InternalIndex]; - } - } - } - - private void RandomizeSoundEffects(TR2RCombinedLevel level) - { - if (_sfxCategories.Count == 0) - { - return; - } - - if (IsUncontrolledLevel(level.Script)) - { - HashSet indices = new(); - foreach (TR2SoundEffect effect in level.Data.SoundEffects.Values) - { - do - { - effect.SampleID = (uint)_generator.Next(0, _numSamples + 1 - Math.Max(effect.SampleCount, 1)); - } - while (!indices.Add(effect.SampleID)); - } - } - else - { - foreach (TR2SFX internalIndex in Enum.GetValues()) - { - TR2SFXDefinition definition = _soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); - if (!level.Data.SoundEffects.ContainsKey(internalIndex) || definition == null - || definition.Creature == TRSFXCreatureCategory.Lara || !_sfxCategories.Contains(definition.PrimaryCategory)) - { - continue; - } - - Predicate pred; - if (Settings.LinkCreatureSFX && definition.Creature > TRSFXCreatureCategory.Lara) - { - pred = sfx => - { - return sfx.Categories.Contains(definition.PrimaryCategory) && - sfx != definition && - ( - sfx.Creature == definition.Creature || - (sfx.Creature == TRSFXCreatureCategory.Lara && definition.Creature == TRSFXCreatureCategory.Human) - ); - }; - } - else - { - pred = sfx => sfx.Categories.Contains(definition.PrimaryCategory) && sfx != definition; - } - - List otherDefinitions = _soundEffects.FindAll(pred); - if (otherDefinitions.Count > 0) - { - TR2SFXDefinition nextDefinition = otherDefinitions[_generator.Next(0, otherDefinitions.Count)]; - if (nextDefinition != definition) - { - level.Data.SoundEffects[internalIndex] = nextDefinition.SoundEffect; - } - } - } - } - } - - private void RandomizeWibble(TR2RCombinedLevel level) - { - if (Settings.RandomizeWibble) - { - foreach (var (_, effect) in level.Data.SoundEffects) - { - effect.RandomizePitch = true; - } - } - } } diff --git a/TRRandomizerCore/Randomizers/TR2/Shared/TR2AudioAllocator.cs b/TRRandomizerCore/Randomizers/TR2/Shared/TR2AudioAllocator.cs new file mode 100644 index 00000000..a6009603 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR2/Shared/TR2AudioAllocator.cs @@ -0,0 +1,187 @@ +using Newtonsoft.Json; +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRRandomizerCore.SFX; + +namespace TRRandomizerCore.Randomizers; + +public class TR2AudioAllocator : AudioRandomizer +{ + private readonly int _numSamples; + + private List _soundEffects; + + public TR2AudioAllocator(IReadOnlyDictionary> tracks, int numSamples) + : base(tracks) + { + _numSamples = numSamples; + } + + protected override string GetAssaultName() + => TR2LevelNames.ASSAULT; + + protected override void LoadData(string backupPath) + { + _soundEffects = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR2\Audio\sfx.json")); + + Dictionary levels = new(); + TR2LevelControl reader = new(); + foreach (TR2SFXDefinition definition in _soundEffects) + { + if (!levels.ContainsKey(definition.SourceLevel)) + { + levels[definition.SourceLevel] = reader.Read(Path.Combine(backupPath, definition.SourceLevel)); + } + + TR2Level level = levels[definition.SourceLevel]; + definition.SoundEffect = level.SoundEffects[definition.InternalIndex]; + } + } + + public void RandomizeMusicTriggers(TR2Level level) + { + RandomizeFloorTracks(level.Rooms, level.FloorData); + RandomizeSecretTracks(level); + } + + public void RandomizeSecretTracks(TR2Level level) + { + if (!Settings.SeparateSecretTracks) + { + return; + } + + List secretTracks = GetTracks(TRAudioCategory.Secret); + Dictionary secrets = GetSecretItems(level); + foreach (int entityIndex in secrets.Keys) + { + TR2Entity secret = secrets[entityIndex]; + TRRoomSector sector = level.GetRoomSector(secret); + if (sector.FDIndex == 0) + { + level.FloorData.CreateFloorData(sector); + } + + List entries = level.FloorData[sector.FDIndex]; + FDTriggerEntry existingTriggerEntry = entries.Find(e => e is FDTriggerEntry) as FDTriggerEntry; + bool existingEntityPickup = false; + if (existingTriggerEntry != null) + { + if (existingTriggerEntry.TrigType == FDTrigType.Pickup && existingTriggerEntry.Actions[0].Parameter == entityIndex) + { + // GW gold secret (default location) already has a pickup trigger to spawn the + // TRex (or whatever enemy) so we'll just append to that item list here + existingEntityPickup = true; + } + else + { + // There is already a non-pickup trigger for this sector so while nothing is wrong with + // adding a pickup trigger, the game ignores it. So in this instance, the sound that + // plays will just be whatever is set in the script. + continue; + } + } + + FDActionItem musicAction = new() + { + Action = FDTrigAction.PlaySoundtrack, + Parameter = (short)secretTracks[Generator.Next(0, secretTracks.Count)].ID + }; + + if (existingEntityPickup) + { + existingTriggerEntry.Actions.Add(musicAction); + } + else + { + entries.Add(new FDTriggerEntry + { + TrigType = FDTrigType.Pickup, + Actions = new() + { + new() { Parameter = (short)entityIndex }, + musicAction + } + }); + } + } + } + + private static Dictionary GetSecretItems(TR2Level level) + { + Dictionary entities = new(); + for (int i = 0; i < level.Entities.Count; i++) + { + if (TR2TypeUtilities.IsSecretType(level.Entities[i].TypeID)) + { + entities[i] = level.Entities[i]; + } + } + + return entities; + } + + public void RandomizeSoundEffects(string levelName, TR2Level level) + { + if (Categories.Count == 0) + { + return; + } + + if (IsUncontrolledLevel(levelName)) + { + // Choose a random but unique pointer into MAIN.SFX for each sample. + HashSet indices = new(); + foreach (TR2SoundEffect effect in level.SoundEffects.Values) + { + do + { + effect.SampleID = (uint)Generator.Next(0, _numSamples + 1 - Math.Max(effect.SampleCount, 1)); + } + while (!indices.Add(effect.SampleID)); + } + } + else + { + foreach (TR2SFX internalIndex in Enum.GetValues()) + { + TR2SFXDefinition definition = _soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); + if (!level.SoundEffects.ContainsKey(internalIndex) || definition == null + || definition.Creature == TRSFXCreatureCategory.Lara || !Categories.Contains(definition.PrimaryCategory)) + { + continue; + } + + Predicate pred; + if (Settings.LinkCreatureSFX && definition.Creature > TRSFXCreatureCategory.Lara) + { + pred = sfx => + { + return sfx.Categories.Contains(definition.PrimaryCategory) && + sfx != definition && + ( + sfx.Creature == definition.Creature || + (sfx.Creature == TRSFXCreatureCategory.Lara && definition.Creature == TRSFXCreatureCategory.Human) + ); + }; + } + else + { + pred = sfx => sfx.Categories.Contains(definition.PrimaryCategory) && sfx != definition; + } + + List otherDefinitions = _soundEffects.FindAll(pred); + if (otherDefinitions.Count > 0) + { + TR2SFXDefinition nextDefinition = otherDefinitions[Generator.Next(0, otherDefinitions.Count)]; + if (nextDefinition != definition) + { + level.SoundEffects[internalIndex] = nextDefinition.SoundEffect; + } + } + } + } + } +} diff --git a/TRRandomizerCore/Randomizers/TR3/Classic/TR3AudioRandomizer.cs b/TRRandomizerCore/Randomizers/TR3/Classic/TR3AudioRandomizer.cs index 49df473a..d353a928 100644 --- a/TRRandomizerCore/Randomizers/TR3/Classic/TR3AudioRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR3/Classic/TR3AudioRandomizer.cs @@ -1,252 +1,31 @@ -using Newtonsoft.Json; -using System.Numerics; -using TRGE.Core; -using TRLevelControl; -using TRLevelControl.Helpers; -using TRLevelControl.Model; -using TRRandomizerCore.Helpers; -using TRRandomizerCore.Levels; -using TRRandomizerCore.SFX; +using TRGE.Core; namespace TRRandomizerCore.Randomizers; public class TR3AudioRandomizer : BaseTR3Randomizer { - private const int _defaultSecretTrack = 122; - private const int _numSamples = 414; - - private AudioRandomizer _audioRandomizer; - private TRAudioTrack _fixedSecretTrack; - - private List _soundEffects; - private List _sfxCategories; - private List _uncontrolledLevels; - public override void Randomize(int seed) { - _generator = new Random(seed); - - LoadAudioData(); - ChooseUncontrolledLevels(); + TR3AudioAllocator allocator = new(ScriptEditor.AudioProvider.GetCategorisedTracks()) + { + Generator = new(seed), + Settings = Settings, + }; + allocator.Initialise(Levels.Select(l => l.LevelFileBaseName), BackupPath); foreach (TR3ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeMusicTriggers(_levelInstance); - - RandomizeSoundEffects(_levelInstance); - - RandomizeWibble(_levelInstance); + allocator.RandomizeMusicTriggers(_levelInstance.Data); + allocator.RandomizeSoundEffects(_levelInstance.Name, _levelInstance.Data); + allocator.RandomizePitch(_levelInstance.Data.SoundEffects.Values); SaveLevelInstance(); - if (!TriggerProgress()) { break; } } } - - private void ChooseUncontrolledLevels() - { - TR3ScriptedLevel assaultCourse = Levels.Find(l => l.Is(TR3LevelNames.ASSAULT)); - ISet exlusions = new HashSet { assaultCourse }; - - _uncontrolledLevels = Levels.RandomSelection(_generator, (int)Settings.UncontrolledSFXCount, exclusions: exlusions); - if (Settings.AssaultCourseWireframe) - { - _uncontrolledLevels.Add(assaultCourse); - } - } - - public bool IsUncontrolledLevel(TR3ScriptedLevel level) - { - return _uncontrolledLevels.Contains(level); - } - - private void RandomizeMusicTriggers(TR3CombinedLevel level) - { - if (Settings.ChangeTriggerTracks) - { - RandomizeFloorTracks(level.Data); - } - - // The secret sound is hardcoded in TR3 to track 122. The workaround for this is to - // always set the secret sound on the corresponding triggers regardless of whether - // or not secret rando is enabled. - RandomizeSecretTracks(level); - } - - private void RandomizeFloorTracks(TR3Level level) - { - _audioRandomizer.ResetFloorMap(); - foreach (TR3Room room in level.Rooms) - { - _audioRandomizer.RandomizeFloorTracks(room.Sectors, level.FloorData, _generator, sectorIndex => - { - // Get the midpoint of the tile in world coordinates - return new Vector2 - ( - TRConsts.Step2 + room.Info.X + sectorIndex / room.NumZSectors * TRConsts.Step4, - TRConsts.Step2 + room.Info.Z + sectorIndex % room.NumZSectors * TRConsts.Step4 - ); - }); - } - } - - private void RandomizeSecretTracks(TR3CombinedLevel level) - { - if (level.Script.NumSecrets == 0) - { - return; - } - - List secretTracks = _audioRandomizer.GetTracks(TRAudioCategory.Secret); - - // If we want the same secret sound throughout the game, select it now. - if (!Settings.SeparateSecretTracks && _fixedSecretTrack == null) - { - _fixedSecretTrack = secretTracks[_generator.Next(0, secretTracks.Count)]; - } - - for (int i = 0; i < level.Script.NumSecrets; i++) - { - // Pick a track for this secret and prepare an action item - TRAudioTrack secretTrack = _fixedSecretTrack ?? secretTracks[_generator.Next(0, secretTracks.Count)]; - if (secretTrack.ID == _defaultSecretTrack) - { - // The game hardcodes this track, so there is no point in amending the triggers. - continue; - } - - FDActionItem musicAction = new() - { - Action = FDTrigAction.PlaySoundtrack, - Parameter = (short)secretTrack.ID - }; - - // Add a music action for each trigger defined for this secret. - List triggers = level.Data.FloorData.GetSecretTriggers(i); - foreach (FDTriggerEntry trigger in triggers) - { - FDActionItem currentMusicAction = trigger.Actions.Find(a => a.Action == FDTrigAction.PlaySoundtrack); - if (currentMusicAction == null) - { - trigger.Actions.Add(musicAction); - } - } - } - } - - private void LoadAudioData() - { - // Get the track data from audio_tracks.json. Loaded from TRGE as it sets the ambient tracks initially. - _audioRandomizer = new AudioRandomizer(ScriptEditor.AudioProvider.GetCategorisedTracks()); - - // Decide which sound effect categories we want to randomize. - _sfxCategories = AudioRandomizer.GetSFXCategories(Settings); - - // Only load the SFX if we are changing at least one category - if (_sfxCategories.Count > 0) - { - _soundEffects = JsonConvert.DeserializeObject>(ReadResource(@"TR3\Audio\sfx.json")); - - Dictionary levels = new(); - TR3LevelControl reader = new(); - foreach (TR3SFXDefinition definition in _soundEffects) - { - if (!levels.ContainsKey(definition.SourceLevel)) - { - levels[definition.SourceLevel] = reader.Read(Path.Combine(BackupPath, definition.SourceLevel)); - } - - TR3Level level = levels[definition.SourceLevel]; - definition.SoundEffect = level.SoundEffects[definition.InternalIndex]; - } - } - } - - private void RandomizeSoundEffects(TR3CombinedLevel level) - { - if (_sfxCategories.Count == 0) - { - // We haven't selected any SFX categories to change. - return; - } - - if (IsUncontrolledLevel(level.Script)) - { - // Choose a random but unique pointer into MAIN.SFX for each sample. - HashSet indices = new(); - foreach (TR3SoundEffect effect in level.Data.SoundEffects.Values) - { - do - { - effect.SampleID = (uint)_generator.Next(0, _numSamples + 1 - Math.Max(effect.SampleCount, 1)); - } - while (!indices.Add(effect.SampleID)); - } - } - else - { - // Run through the SoundMap for this level and get the SFX definition for each one. - // Choose a new sound effect provided the definition is in a category we want to change. - // Lara's SFX are not changed by default. - foreach (TR3SFX internalIndex in Enum.GetValues()) - { - TR3SFXDefinition definition = _soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); - if (!level.Data.SoundEffects.ContainsKey(internalIndex) || definition == null - || definition.Creature == TRSFXCreatureCategory.Lara || !_sfxCategories.Contains(definition.PrimaryCategory)) - { - continue; - } - - // The following allows choosing to keep humans making human noises, and animals animal noises. - // Other humans can use Lara's SFX. - Predicate pred; - if (Settings.LinkCreatureSFX && definition.Creature > TRSFXCreatureCategory.Lara) - { - pred = sfx => - { - return sfx.Categories.Contains(definition.PrimaryCategory) && - sfx != definition && - ( - sfx.Creature == definition.Creature || - (sfx.Creature == TRSFXCreatureCategory.Lara && definition.Creature == TRSFXCreatureCategory.Human) - ); - }; - } - else - { - pred = sfx => sfx.Categories.Contains(definition.PrimaryCategory) && sfx != definition; - } - - // Try to find definitions that match - List otherDefinitions = _soundEffects.FindAll(pred); - if (otherDefinitions.Count > 0) - { - // Pick a new definition and try to import it into the level. This should only fail if - // the JSON is misconfigured e.g. missing sample indices. In that case, we just leave - // the current sound effect as-is. - TR3SFXDefinition nextDefinition = otherDefinitions[_generator.Next(0, otherDefinitions.Count)]; - if (nextDefinition != definition) - { - level.Data.SoundEffects[internalIndex] = nextDefinition.SoundEffect; - } - } - } - } - } - - private void RandomizeWibble(TR3CombinedLevel level) - { - if (Settings.RandomizeWibble) - { - foreach (var (_, effect) in level.Data.SoundEffects) - { - effect.RandomizePitch = true; - } - } - } } diff --git a/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RAudioRandomizer.cs b/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RAudioRandomizer.cs index 4411f586..93244e6f 100644 --- a/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RAudioRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR3/Remastered/TR3RAudioRandomizer.cs @@ -1,40 +1,25 @@ -using Newtonsoft.Json; -using System.Numerics; -using TRGE.Core; -using TRLevelControl; -using TRLevelControl.Helpers; -using TRLevelControl.Model; -using TRRandomizerCore.Helpers; -using TRRandomizerCore.Levels; -using TRRandomizerCore.SFX; +using TRGE.Core; namespace TRRandomizerCore.Randomizers; public class TR3RAudioRandomizer : BaseTR3RRandomizer { - private const int _defaultSecretTrack = 122; - private const int _numSamples = 414; - - private AudioRandomizer _audioRandomizer; - - private List _soundEffects; - private List _sfxCategories; - private List _uncontrolledLevels; - public override void Randomize(int seed) { - _generator = new(seed); - - LoadAudioData(); - ChooseUncontrolledLevels(); + TR3AudioAllocator allocator = new(ScriptEditor.AudioProvider.GetCategorisedTracks()) + { + Generator = new(seed), + Settings = Settings, + }; + allocator.Initialise(Levels.Select(l => l.LevelFileBaseName), BackupPath); foreach (TRRScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeMusicTriggers(_levelInstance); - RandomizeSoundEffects(_levelInstance); - RandomizeWibble(_levelInstance); + allocator.RandomizeMusicTriggers(_levelInstance.Data); + allocator.RandomizeSoundEffects(_levelInstance.Name, _levelInstance.Data); + allocator.RandomizePitch(_levelInstance.Data.SoundEffects.Values); SaveLevelInstance(); if (!TriggerProgress()) @@ -43,177 +28,4 @@ public override void Randomize(int seed) } } } - - private void ChooseUncontrolledLevels() - { - TRRScriptedLevel assaultCourse = Levels.Find(l => l.Is(TR3LevelNames.ASSAULT)); - HashSet exlusions = new () { assaultCourse }; - - _uncontrolledLevels = Levels.RandomSelection(_generator, (int)Settings.UncontrolledSFXCount, exclusions: exlusions); - if (Settings.AssaultCourseWireframe) - { - _uncontrolledLevels.Add(assaultCourse); - } - } - - public bool IsUncontrolledLevel(TRRScriptedLevel level) - { - return _uncontrolledLevels.Contains(level); - } - - private void RandomizeMusicTriggers(TR3RCombinedLevel level) - { - if (Settings.ChangeTriggerTracks) - { - RandomizeFloorTracks(level.Data); - } - - if (Settings.SeparateSecretTracks) - { - RandomizeSecretTracks(level); - } - } - - private void RandomizeFloorTracks(TR3Level level) - { - _audioRandomizer.ResetFloorMap(); - foreach (TR3Room room in level.Rooms) - { - _audioRandomizer.RandomizeFloorTracks(room.Sectors, level.FloorData, _generator, sectorIndex => - { - return new Vector2 - ( - TRConsts.Step2 + room.Info.X + sectorIndex / room.NumZSectors * TRConsts.Step4, - TRConsts.Step2 + room.Info.Z + sectorIndex % room.NumZSectors * TRConsts.Step4 - ); - }); - } - } - - private void RandomizeSecretTracks(TR3RCombinedLevel level) - { - List secretTracks = _audioRandomizer.GetTracks(TRAudioCategory.Secret); - - for (int i = 0; i < level.Script.NumSecrets; i++) - { - TRAudioTrack secretTrack = secretTracks[_generator.Next(0, secretTracks.Count)]; - if (secretTrack.ID == _defaultSecretTrack) - { - continue; - } - - FDActionItem musicAction = new() - { - Action = FDTrigAction.PlaySoundtrack, - Parameter = (short)secretTrack.ID - }; - - List triggers = level.Data.FloorData.GetSecretTriggers(i); - foreach (FDTriggerEntry trigger in triggers) - { - FDActionItem currentMusicAction = trigger.Actions.Find(a => a.Action == FDTrigAction.PlaySoundtrack); - if (currentMusicAction == null) - { - trigger.Actions.Add(musicAction); - } - } - } - } - - private void LoadAudioData() - { - _audioRandomizer = new AudioRandomizer(ScriptEditor.AudioProvider.GetCategorisedTracks()); - _sfxCategories = AudioRandomizer.GetSFXCategories(Settings); - if (_sfxCategories.Count > 0) - { - _soundEffects = JsonConvert.DeserializeObject>(ReadResource(@"TR3\Audio\sfx.json")); - - Dictionary levels = new(); - TR3LevelControl reader = new(); - foreach (TR3SFXDefinition definition in _soundEffects) - { - if (!levels.ContainsKey(definition.SourceLevel)) - { - levels[definition.SourceLevel] = reader.Read(Path.Combine(BackupPath, definition.SourceLevel)); - } - - TR3Level level = levels[definition.SourceLevel]; - definition.SoundEffect = level.SoundEffects[definition.InternalIndex]; - } - } - } - - private void RandomizeSoundEffects(TR3RCombinedLevel level) - { - if (_sfxCategories.Count == 0) - { - return; - } - - if (IsUncontrolledLevel(level.Script)) - { - HashSet indices = new(); - foreach (TR3SoundEffect effect in level.Data.SoundEffects.Values) - { - uint sampleIndex; - do - { - sampleIndex = (uint)_generator.Next(0, _numSamples + 1 - Math.Max(effect.SampleCount, 1)); - } - while (!indices.Add(sampleIndex)); - effect.SampleID = sampleIndex; - } - } - else - { - foreach (TR3SFX internalIndex in Enum.GetValues()) - { - TR3SFXDefinition definition = _soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); - if (!level.Data.SoundEffects.ContainsKey(internalIndex) || definition == null - || definition.Creature == TRSFXCreatureCategory.Lara || !_sfxCategories.Contains(definition.PrimaryCategory)) - { - continue; - } - - Predicate pred; - if (Settings.LinkCreatureSFX && definition.Creature > TRSFXCreatureCategory.Lara) - { - pred = sfx => - { - return sfx.Categories.Contains(definition.PrimaryCategory) && - sfx != definition && - ( - sfx.Creature == definition.Creature || - (sfx.Creature == TRSFXCreatureCategory.Lara && definition.Creature == TRSFXCreatureCategory.Human) - ); - }; - } - else - { - pred = sfx => sfx.Categories.Contains(definition.PrimaryCategory) && sfx != definition; - } - - List otherDefinitions = _soundEffects.FindAll(pred); - if (otherDefinitions.Count > 0) - { - TR3SFXDefinition nextDefinition = otherDefinitions[_generator.Next(0, otherDefinitions.Count)]; - if (nextDefinition != definition) - { - level.Data.SoundEffects[internalIndex] = nextDefinition.SoundEffect; - } - } - } - } - } - - private void RandomizeWibble(TR3RCombinedLevel level) - { - if (Settings.RandomizeWibble) - { - foreach (var (_, effect) in level.Data.SoundEffects) - { - effect.RandomizePitch = true; - } - } - } } diff --git a/TRRandomizerCore/Randomizers/TR3/Shared/TR3AudioAllocator.cs b/TRRandomizerCore/Randomizers/TR3/Shared/TR3AudioAllocator.cs new file mode 100644 index 00000000..c8748afe --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR3/Shared/TR3AudioAllocator.cs @@ -0,0 +1,110 @@ +using Newtonsoft.Json; +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRRandomizerCore.SFX; + +namespace TRRandomizerCore.Randomizers; + +public class TR3AudioAllocator : AudioRandomizer +{ + private const int _defaultSecretTrack = 122; + private const int _numSamples = 414; + + private List _soundEffects; + + public TR3AudioAllocator(IReadOnlyDictionary> tracks) + : base(tracks) { } + + protected override string GetAssaultName() + => TR3LevelNames.ASSAULT; + + protected override void LoadData(string backupPath) + { + _soundEffects = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR3\Audio\sfx.json")); + + Dictionary levels = new(); + TR3LevelControl reader = new(); + foreach (TR3SFXDefinition definition in _soundEffects) + { + if (!levels.ContainsKey(definition.SourceLevel)) + { + levels[definition.SourceLevel] = reader.Read(Path.Combine(backupPath, definition.SourceLevel)); + } + + TR3Level level = levels[definition.SourceLevel]; + definition.SoundEffect = level.SoundEffects[definition.InternalIndex]; + } + } + + public void RandomizeMusicTriggers(TR3Level level) + { + RandomizeFloorTracks(level.Rooms, level.FloorData); + if (!Settings.RandomizeSecrets) + { + RandomizeSecretTracks(level.FloorData, _defaultSecretTrack); + } + } + + public void RandomizeSoundEffects(string levelName, TR3Level level) + { + if (Categories.Count == 0) + { + return; + } + + if (IsUncontrolledLevel(levelName)) + { + HashSet indices = new(); + foreach (TR3SoundEffect effect in level.SoundEffects.Values) + { + do + { + effect.SampleID = (uint)Generator.Next(0, _numSamples + 1 - Math.Max(effect.SampleCount, 1)); + } + while (!indices.Add(effect.SampleID)); + } + } + else + { + foreach (TR3SFX internalIndex in Enum.GetValues()) + { + TR3SFXDefinition definition = _soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); + if (!level.SoundEffects.ContainsKey(internalIndex) || definition == null + || definition.Creature == TRSFXCreatureCategory.Lara || !Categories.Contains(definition.PrimaryCategory)) + { + continue; + } + + Predicate pred; + if (Settings.LinkCreatureSFX && definition.Creature > TRSFXCreatureCategory.Lara) + { + pred = sfx => + { + return sfx.Categories.Contains(definition.PrimaryCategory) && + sfx != definition && + ( + sfx.Creature == definition.Creature || + (sfx.Creature == TRSFXCreatureCategory.Lara && definition.Creature == TRSFXCreatureCategory.Human) + ); + }; + } + else + { + pred = sfx => sfx.Categories.Contains(definition.PrimaryCategory) && sfx != definition; + } + + List otherDefinitions = _soundEffects.FindAll(pred); + if (otherDefinitions.Count > 0) + { + TR3SFXDefinition nextDefinition = otherDefinitions[Generator.Next(0, otherDefinitions.Count)]; + if (nextDefinition != definition) + { + level.SoundEffects[internalIndex] = nextDefinition.SoundEffect; + } + } + } + } + } +} diff --git a/TRRandomizerCore/TRVersionSupport.cs b/TRRandomizerCore/TRVersionSupport.cs index 0d7ed727..06173fe2 100644 --- a/TRRandomizerCore/TRVersionSupport.cs +++ b/TRRandomizerCore/TRVersionSupport.cs @@ -42,6 +42,7 @@ internal class TRVersionSupport TRRandomizerType.ReturnPaths, TRRandomizerType.RewardRooms, TRRandomizerType.Secret, + TRRandomizerType.SecretAudio, TRRandomizerType.SecretCount, TRRandomizerType.SecretModels, TRRandomizerType.SecretReward, @@ -69,6 +70,7 @@ internal class TRVersionSupport TRRandomizerType.Secret, TRRandomizerType.SecretAudio, TRRandomizerType.SecretReward, + TRRandomizerType.SFX, TRRandomizerType.StartPosition, }; diff --git a/TRRandomizerView/Model/ControllerOptions.cs b/TRRandomizerView/Model/ControllerOptions.cs index 1ffe5ccd..eacd3475 100644 --- a/TRRandomizerView/Model/ControllerOptions.cs +++ b/TRRandomizerView/Model/ControllerOptions.cs @@ -1649,7 +1649,9 @@ public bool RandomizeSecrets set { _randomSecretsControl.IsActive = value; + _separateSecretTracks.IsActive = IsTR2 || !value; FirePropertyChanged(); + FirePropertyChanged(nameof(SeparateSecretTracks)); } }