diff --git a/Deps/TRGE.Coord.dll b/Deps/TRGE.Coord.dll index cad851020..331968e09 100644 Binary files a/Deps/TRGE.Coord.dll and b/Deps/TRGE.Coord.dll differ diff --git a/Deps/TRGE.Core.dll b/Deps/TRGE.Core.dll index 828bad970..58c2c6071 100644 Binary files a/Deps/TRGE.Core.dll and b/Deps/TRGE.Core.dll differ diff --git a/SFXExport/Program.cs b/SFXExport/Program.cs index 21de183a4..b890dd5e5 100644 --- a/SFXExport/Program.cs +++ b/SFXExport/Program.cs @@ -14,20 +14,28 @@ static void Main(string[] args) } string file = args[0]; - string exportDir = args[1]; - if (Directory.Exists(exportDir)) + TRGameVersion version = Enum.Parse(args[1]); + if (!Enum.IsDefined(typeof(TRGameVersion), version)) { - Directory.Delete(exportDir, true); + Console.WriteLine("Unrecognised game verion."); + return; + } + + if (Directory.Exists(version.ToString())) + { + Directory.Delete(version.ToString(), true); } - Directory.CreateDirectory(exportDir); + Directory.CreateDirectory(version.ToString()); + + bool remaster = args.Length > 2 && args[2].ToUpper() == "REMASTER"; switch (Path.GetExtension(file).ToUpper()) { case ".SFX": - ExtractFromSFX(file, exportDir); + ExtractFromSFX(file, version, remaster); break; case ".PHD": - ExtractFromPHD(file, exportDir); + ExtractFromPHD(file, version); break; default: Console.WriteLine("Unsupported file."); @@ -35,10 +43,17 @@ static void Main(string[] args) } } - private static void ExtractFromSFX(string file, string exportDir) + private static void ExtractFromSFX(string file, TRGameVersion version, bool remaster) { int sample = 0; using BinaryReader reader = new(File.Open(file, FileMode.Open)); + + if (remaster) + { + // Header remains unknown for now + reader.BaseStream.Position = version == TRGameVersion.TR1 ? 0x200 : 0x2E4; + } + while (reader.BaseStream.Position < reader.BaseStream.Length) { uint[] header = new uint[11]; @@ -47,7 +62,7 @@ private static void ExtractFromSFX(string file, string exportDir) header[i] = reader.ReadUInt32(); } - using BinaryWriter writer = new(File.Create(Path.Combine(exportDir, sample++ + ".wav"))); + using BinaryWriter writer = new(File.Create(Path.Combine(version.ToString(), sample++ + ".wav"))); for (int i = 0; i < header.Length; i++) { writer.Write(header[i]); @@ -61,7 +76,7 @@ private static void ExtractFromSFX(string file, string exportDir) } } - private static void ExtractFromPHD(string file, string exportDir) + private static void ExtractFromPHD(string file, TRGameVersion version) { TR1Level level = new TR1LevelControl().Read(file); for (int i = 0; i < level.NumSampleIndices; i++) @@ -73,7 +88,7 @@ private static void ExtractFromPHD(string file, string exportDir) sampleEnd = (uint)level.Samples.Length; } - using BinaryWriter writer = new(File.Create(Path.Combine(exportDir, i + ".wav"))); + using BinaryWriter writer = new(File.Create(Path.Combine(version.ToString(), i + ".wav"))); for (uint j = sampleStart; j < sampleEnd; j++) { writer.Write(level.Samples[j]); @@ -84,7 +99,7 @@ private static void ExtractFromPHD(string file, string exportDir) private static void Usage() { Console.WriteLine(); - Console.WriteLine("Usage: SFXExport [*.SFX|*.PHD] [EXPORT_DIR]"); + Console.WriteLine("Usage: SFXExport [*.SFX|*.PHD] [TR1|TR2|TR3] [REMASTER|CLASSIC]"); Console.WriteLine(); Console.WriteLine("Example"); diff --git a/TRLevelControl/Helpers/TRConsts.cs b/TRLevelControl/Helpers/TRConsts.cs index 2bc0c38d3..356b39162 100644 --- a/TRLevelControl/Helpers/TRConsts.cs +++ b/TRLevelControl/Helpers/TRConsts.cs @@ -22,4 +22,6 @@ public static class TRConsts public const int MaskBits = 5; public const int FullMask = (1 << MaskBits) - 1; + + public const int Angle45 = 8192; } diff --git a/TRRandomizerCore/Editors/TR1RandoEditor.cs b/TRRandomizerCore/Editors/TR1ClassicEditor.cs similarity index 98% rename from TRRandomizerCore/Editors/TR1RandoEditor.cs rename to TRRandomizerCore/Editors/TR1ClassicEditor.cs index d089b631d..d2ca4df34 100644 --- a/TRRandomizerCore/Editors/TR1RandoEditor.cs +++ b/TRRandomizerCore/Editors/TR1ClassicEditor.cs @@ -11,19 +11,19 @@ namespace TRRandomizerCore.Editors; -public class TR1RandoEditor : TR1LevelEditor, ISettingsProvider +public class TR1ClassicEditor : TR1LevelEditor, ISettingsProvider { private static readonly Point _regularBadgePos = new(706, 537); private static readonly Point _goldBadgePos = new(498, 445); public RandomizerSettings Settings { get; private set; } - public TR1RandoEditor(TRDirectoryIOArgs args, TREdition edition) + public TR1ClassicEditor(TRDirectoryIOArgs args, TREdition edition) : base(args, edition) { } protected override void ApplyConfig(Config config) { - Settings = new RandomizerSettings() + Settings = new() { ExcludableEnemies = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR1\Restrictions\excludable_enemies.json")) }; diff --git a/TRRandomizerCore/Editors/TR1RemasteredEditor.cs b/TRRandomizerCore/Editors/TR1RemasteredEditor.cs new file mode 100644 index 000000000..3449eaf7a --- /dev/null +++ b/TRRandomizerCore/Editors/TR1RemasteredEditor.cs @@ -0,0 +1,138 @@ +using TRGE.Core; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Randomizers; + +namespace TRRandomizerCore.Editors; + +public class TR1RemasteredEditor : TR1ClassicEditor +{ + public TR1RemasteredEditor(TRDirectoryIOArgs io, TREdition edition) + : base(io, edition) { } + + protected override void ApplyConfig(Config config) + { + base.ApplyConfig(config); + Settings.AllowReturnPathLocations = false; + Settings.AddReturnPaths = false; + Settings.FixOGBugs = false; + } + + protected override int GetSaveTarget(int numLevels) + { + int target = 0; + + if (Settings.RandomizeItems) + { + target += numLevels; + if (Settings.IncludeKeyItems) + { + target += numLevels; + } + } + + if (Settings.RandomizeStartPosition) + { + target += numLevels; + } + + if (Settings.RandomizeAudio) + { + target += numLevels; + } + + // Environment randomizer always runs + target += numLevels * 2; + + return target; + } + + protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMonitor monitor) + { + List levels = new( + scriptEditor.EnabledScriptedLevels.Cast().ToList() + ); + + if (scriptEditor.GymAvailable) + { + levels.Add(scriptEditor.AssaultLevel as TRRScriptedLevel); + } + + string backupDirectory = _io.BackupDirectory.FullName; + string wipDirectory = _io.WIPOutputDirectory.FullName; + + ItemFactory itemFactory = new(@"Resources\TR1\Items\repurposable_items.json"); + TR1RItemRandomizer itemRandomizer = new() + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings, + ItemFactory = itemFactory + }; + + TR1REnvironmentRandomizer environmentRandomizer = new() + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings, + }; + + if (!monitor.IsCancelled && Settings.RandomizeItems) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing standard items"); + itemRandomizer.Randomize(Settings.ItemSeed); + } + + if (!monitor.IsCancelled && Settings.RandomizeStartPosition) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing start positions"); + new TR1RStartPositionRandomizer + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings + }.Randomize(Settings.StartPositionSeed); + } + + if (!monitor.IsCancelled) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, Settings.RandomizeEnvironment ? "Randomizing environment" : "Applying default environment packs"); + environmentRandomizer.Randomize(Settings.EnvironmentSeed); + } + + if (!monitor.IsCancelled && Settings.RandomizeItems && Settings.IncludeKeyItems) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing key items"); + itemRandomizer.RandomizeKeyItems(); + } + + if (!monitor.IsCancelled) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Finalizing environment changes"); + environmentRandomizer.FinalizeEnvironment(); + } + + if (!monitor.IsCancelled && Settings.RandomizeAudio) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing audio tracks"); + new TR1RAudioRandomizer + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings + }.Randomize(Settings.AudioSeed); + } + } +} diff --git a/TRRandomizerCore/Editors/TR2RandoEditor.cs b/TRRandomizerCore/Editors/TR2ClassicEditor.cs similarity index 98% rename from TRRandomizerCore/Editors/TR2RandoEditor.cs rename to TRRandomizerCore/Editors/TR2ClassicEditor.cs index a2a5baab9..01bdaae69 100644 --- a/TRRandomizerCore/Editors/TR2RandoEditor.cs +++ b/TRRandomizerCore/Editors/TR2ClassicEditor.cs @@ -9,16 +9,16 @@ namespace TRRandomizerCore.Editors; -public class TR2RandoEditor : TR2LevelEditor, ISettingsProvider +public class TR2ClassicEditor : TR2LevelEditor, ISettingsProvider { public RandomizerSettings Settings { get; private set; } - public TR2RandoEditor(TRDirectoryIOArgs args, TREdition edition) + public TR2ClassicEditor(TRDirectoryIOArgs args, TREdition edition) : base(args, edition) { } protected override void ApplyConfig(Config config) { - Settings = new RandomizerSettings + Settings = new() { ExcludableEnemies = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR2\Restrictions\excludable_enemies.json")) }; diff --git a/TRRandomizerCore/Editors/TR2RemasteredEditor.cs b/TRRandomizerCore/Editors/TR2RemasteredEditor.cs new file mode 100644 index 000000000..cad8597dc --- /dev/null +++ b/TRRandomizerCore/Editors/TR2RemasteredEditor.cs @@ -0,0 +1,141 @@ +using TRGE.Core; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Randomizers; + +namespace TRRandomizerCore.Editors; + +public class TR2RemasteredEditor : TR2ClassicEditor +{ + public TR2RemasteredEditor(TRDirectoryIOArgs io, TREdition edition) + : base(io, edition) { } + + protected override void ApplyConfig(Config config) + { + base.ApplyConfig(config); + Settings.AllowReturnPathLocations = false; + Settings.AddReturnPaths = false; + Settings.FixOGBugs = false; + } + + protected override int GetSaveTarget(int numLevels) + { + int target = 0; + + if (Settings.RandomizeItems) + { + target += numLevels; + if (Settings.IncludeKeyItems) + { + target += numLevels; + } + } + + if (Settings.RandomizeStartPosition) + { + target += numLevels; + } + + if (Settings.RandomizeAudio) + { + target += numLevels; + } + + // Environment randomizer always runs + target += numLevels * 2; + + return target; + } + + protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMonitor monitor) + { + List levels = new( + scriptEditor.EnabledScriptedLevels.Cast().ToList() + ); + + if (scriptEditor.GymAvailable) + { + levels.Add(scriptEditor.AssaultLevel as TRRScriptedLevel); + } + + string backupDirectory = _io.BackupDirectory.FullName; + string wipDirectory = _io.WIPOutputDirectory.FullName; + + ItemFactory itemFactory = new() + { + DefaultItem = new() { Intensity1 = -1, Intensity2 = -1 } + }; + TR2RItemRandomizer itemRandomizer = new() + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings, + ItemFactory = itemFactory, + }; + + TR2REnvironmentRandomizer environmentRandomizer = new() + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings, + }; + + if (!monitor.IsCancelled && Settings.RandomizeItems) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing standard items"); + itemRandomizer.Randomize(Settings.ItemSeed); + } + + if (!monitor.IsCancelled && Settings.RandomizeStartPosition) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing start positions"); + new TR2RStartPositionRandomizer + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings + }.Randomize(Settings.StartPositionSeed); + } + + if (!monitor.IsCancelled) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, Settings.RandomizeEnvironment ? "Randomizing environment" : "Applying default environment packs"); + environmentRandomizer.Randomize(Settings.EnvironmentSeed); + } + + if (!monitor.IsCancelled && Settings.RandomizeItems && Settings.IncludeKeyItems) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing key items"); + itemRandomizer.RandomizeKeyItems(); + } + + if (!monitor.IsCancelled) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Finalizing environment changes"); + environmentRandomizer.FinalizeEnvironment(); + } + + if (!monitor.IsCancelled && Settings.RandomizeAudio) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing audio tracks"); + new TR2RAudioRandomizer + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings + }.Randomize(Settings.AudioSeed); + } + } +} diff --git a/TRRandomizerCore/Editors/TR3RandoEditor.cs b/TRRandomizerCore/Editors/TR3ClassicEditor.cs similarity index 98% rename from TRRandomizerCore/Editors/TR3RandoEditor.cs rename to TRRandomizerCore/Editors/TR3ClassicEditor.cs index 1f36c268e..1a80aeeb6 100644 --- a/TRRandomizerCore/Editors/TR3RandoEditor.cs +++ b/TRRandomizerCore/Editors/TR3ClassicEditor.cs @@ -9,16 +9,16 @@ namespace TRRandomizerCore.Editors; -public class TR3RandoEditor : TR3LevelEditor, ISettingsProvider +public class TR3ClassicEditor : TR3LevelEditor, ISettingsProvider { public RandomizerSettings Settings { get; private set; } - public TR3RandoEditor(TRDirectoryIOArgs args, TREdition edition) + public TR3ClassicEditor(TRDirectoryIOArgs args, TREdition edition) : base(args, edition) { } protected override void ApplyConfig(Config config) { - Settings = new RandomizerSettings + Settings = new() { ExcludableEnemies = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR3\Restrictions\excludable_enemies.json")) }; diff --git a/TRRandomizerCore/Editors/TR3RemasteredEditor.cs b/TRRandomizerCore/Editors/TR3RemasteredEditor.cs new file mode 100644 index 000000000..43e3a416c --- /dev/null +++ b/TRRandomizerCore/Editors/TR3RemasteredEditor.cs @@ -0,0 +1,142 @@ +using TRGE.Core; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Randomizers; + +namespace TRRandomizerCore.Editors; + +public class TR3RemasteredEditor : TR3ClassicEditor +{ + public TR3RemasteredEditor(TRDirectoryIOArgs io, TREdition edition) + : base(io, edition) { } + + protected override void ApplyConfig(Config config) + { + base.ApplyConfig(config); + Settings.AllowReturnPathLocations = false; + Settings.AddReturnPaths = false; + Settings.FixOGBugs = false; + } + + protected override int GetSaveTarget(int numLevels) + { + int target = 0; + + if (Settings.RandomizeItems) + { + target += numLevels; + if (Settings.IncludeKeyItems) + { + target += numLevels; + } + } + + if (Settings.RandomizeStartPosition) + { + target += numLevels; + } + + if (Settings.RandomizeAudio) + { + target += numLevels; + } + + // Environment randomizer always runs + target += numLevels * 2; + + return target; + } + + protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMonitor monitor) + { + List levels = new( + scriptEditor.EnabledScriptedLevels.Cast().ToList() + ); + + if (scriptEditor.GymAvailable) + { + levels.Add(scriptEditor.AssaultLevel as TRRScriptedLevel); + } + + string backupDirectory = _io.BackupDirectory.FullName; + string wipDirectory = _io.WIPOutputDirectory.FullName; + + ItemFactory itemFactory = new(@"Resources\TR3\Items\repurposable_items.json") + { + DefaultItem = new() { Intensity1 = -1, Intensity2 = -1 } + }; + + TR3RItemRandomizer itemRandomizer = new() + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings, + ItemFactory = itemFactory, + }; + + TR3REnvironmentRandomizer environmentRandomizer = new() + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings, + }; + + if (!monitor.IsCancelled && Settings.RandomizeItems) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing standard items"); + itemRandomizer.Randomize(Settings.ItemSeed); + } + + if (!monitor.IsCancelled && Settings.RandomizeStartPosition) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing start positions"); + new TR3RStartPositionRandomizer + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings + }.Randomize(Settings.StartPositionSeed); + } + + if (!monitor.IsCancelled) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, Settings.RandomizeEnvironment ? "Randomizing environment" : "Applying default environment packs"); + environmentRandomizer.Randomize(Settings.EnvironmentSeed); + } + + if (!monitor.IsCancelled && Settings.RandomizeItems && Settings.IncludeKeyItems) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing key items"); + itemRandomizer.RandomizeKeyItems(); + } + + if (!monitor.IsCancelled) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Finalizing environment changes"); + environmentRandomizer.FinalizeEnvironment(); + } + + if (!monitor.IsCancelled && Settings.RandomizeAudio) + { + monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Randomizing audio tracks"); + new TR3RAudioRandomizer + { + ScriptEditor = scriptEditor, + Levels = levels, + BasePath = wipDirectory, + BackupPath = backupDirectory, + SaveMonitor = monitor, + Settings = Settings + }.Randomize(Settings.AudioSeed); + } + } +} diff --git a/TRRandomizerCore/Helpers/ChecksumTester.cs b/TRRandomizerCore/Helpers/ChecksumTester.cs index b8faa2658..ad60b12c8 100644 --- a/TRRandomizerCore/Helpers/ChecksumTester.cs +++ b/TRRandomizerCore/Helpers/ChecksumTester.cs @@ -48,6 +48,44 @@ public bool Test(string file) "7f7c535bcab001698a1f965053972f49", // END.PHD "f03f7a545330d5ba30e2aec62d47b4a3", // END2.PHD + // TR1R V1.0+ + "16e1ec56fe8f00c97fb618e16f58ceed", // GYM.PHD + "b58c2461c5b01c378317f2f8a1a1fc55", // LEVEL1.PHD + "24ae96520c0f041d166f1ce3be09db84", // LEVEL2.PHD + "83b2e26a60a5fb30b19153c1ee945d5f", // LEVEL3A.PHD + "41949bf9de7ad208379491d20781c074", // LEVEL3B.PHD + "e7a2fed3f3b40c3367def9d6dfdd98e9", // LEVEL4.PHD + "115519647c6aa7e7c6da38dd00c55f26", // LEVEL5.PHD + "bd78c050343d8ff69de6fc8805e8dd18", // LEVEL6.PHD + "a02bcb8de92fa9dc9e410d5f8795537f", // LEVEL7A.PHD + "e9cee17932c3b6bb02e5a9beea1742b2", // LEVEL7B.PHD + "51a8aaa4a5f510fb6f4eeeea1985a4d9", // LEVEL8A.PHD + "58f050471880ba5d0c9677aaadf1ac4a", // LEVEL8B.PHD + "0cc53fac2255b1be1dbc032b0d5cdc01", // LEVEL8C.PHD + "cd134e858da2137b7c9601b8e9c3d273", // LEVEL10A.PHD + "ec8b196335968127c515739d7af3f55e", // LEVEL10B.PHD + "11a4d4674ba47dbe2da19c64eb7e5970", // CUT4.PHD + "c750c2fe90ab7d51dcfa2a9f35cbfe55", // LEVEL10C.PHD + "94ae1b3e5438457db2c110be20e5a93c", // EGYPT.PHD + "43072a2205d4af2d2fdba63e4ebf5c11", // CAT.PHD + "cba8ce583dbab726ffa2f0a3798a1572", // END.PHD + "2292ba13707de8524c51b1ffa9a95446", // END2.PHD + + // TR1R V1.1+ + "ed82f9c85457e4ec85b47f66090d1a11", // LEVEL1.PHD + "8124261717c6cd71ddc9eb95d76ef5e0", // LEVEL3B.PHD + "4f4304771122d0679826dfbf586f9d91", // LEVEL4.PHD + "f4144cba464d7eb8ecd4d37c57174ea8", // LEVEL5.PHD + "e06a9a1123b6cb1b1c57893bc1f1b0d1", // LEVEL8B.PHD + "9e5e30e25087490f0e9654e23951ccc6", // LEVEL8C.PHD + "126658bcdc6a6a32e8809d0b29c6a993", // LEVEL10A.PHD + "3d7fa5acc7d78c7f809ef08077a99fea", // LEVEL10B.PHD + "e3ad7ded14954ece42abf0d41f7ab55f", // LEVEL10C.PHD + + // TR1R V1.2+ + "8376349b1989024228f7135f9c7187a6", // LEVEL3A.PHD + "0878ce6127a93335b6a96f4dfbe61fb9", // LEVEL8C.PHD + // TR2 "d6f218e32d172e67db60daa35ef7e114", // ASSAULT.TR2 "da1e01683dad5fabbfff89c267b75b9e", // WALL.TR2 @@ -79,6 +117,35 @@ public bool Test(string file) "63ff8be5c7abf37d7065456afeda029f", // LEVEL4.TR2 "88c8124b6d152b9f3fe1c4eeec4428a5", // LEVEL5.TR2 + // TR2R V1.0+ + "eaa29bf1528f38230d7333689d047272", // WALL.TR2 + "3aaf0aee44fbd2f52b3e17cb44b98e3b", // BOAT.TR2 + "cab8e60a93bf0d57e6596770060cc174", // VENICE.TR2 + "e2c74ab8e0fb81c45e85d1644392812a", // OPERA.TR2 + "7eee55f021b37a71068f4df9e628268d", // RIG.TR2 + "4edcca946398df186af62a50a5a0efcc", // PLATFORM.TR2 + "54b750469b48153af58af6922b4c1dff", // UNWATER.TR2 + "90b734cbe488b2a926a8d02d7c7b24f8", // KEEL.TR2 + "3bb6c263b3758e781e0823cd0e4e5366", // LIVING.TR2 + "5f3b660dac21f1ff182eef75343913f4", // DECK.TR2 + "f60792466773c2fdd476ae11f9ad22e7", // SKIDOO.TR2 + "742ae5026fd241558c473ff89e6b868e", // MONASTRY.TR2 + "629eefb9558a215e452c87c35bfcc95a", // CATACOMB.TR2 + "e26bf56e74719cd4f7a11c2d1135143a", // ICECAVE.TR2 + "3d3413eb305b54e0fc67d028accf413b", // EMPRTOMB.TR2 + "4756d792860b1a8bb86effa7bdd0ce80", // FLOATING.TR2 + "25c5f07faecbe656e1c2190378d01e34", // XIAN.TR2 + "07f7fde736c6a3a9c46f877bf275e1aa", // HOUSE.TR2 + "ecf84476a8ca3eb1330d0078203d36e3", // LEVEL1.TR2 + "338df62a6a7bd6eb4b7d2f303eeec429", // LEVEL2.TR2 + "f0cb2aafd4ddbb6cd09b1d21d9eda8c6", // LEVEL3.TR2 + "a43bacce79dd110f55fdf635b95523bf", // LEVEL4.TR2 + "e05c5ec3c10e3043a42791a1b649ddc7", // LEVEL5.TR2 + + // TR2R V1.1+ + "79e3f05f963acac6f864ba81555cc15d", // MONASTRY.TR2 + "cf4f784bdfa0b5794b3437e4d9ed04d8", // ICECAVE.TR2 + // TR3 "5e11d251ddb12b98ebead1883dc12d2a", // HOUSE.TR2 "9befdc5075fdb84450d2ed0533719b12", // JUNGLE.TR2 (International) @@ -126,5 +193,39 @@ public bool Test(string file) "a83c32d6306e18e0a15d79138ac131cb", // UNDERSEA.TR2 "7cfb31fbd9031900c602eea32d1543ec", // ZOO.TR2 "17286cb4cd4f079cf208fdaf38e53d2b", // SLINC.TR2 + + // TR3R V1.0+ + "7894cb25312114d969b79d7a7fb17f67", // HOUSE.TR2 + "d9d5f08872ee43f6c27cf3b22cb9ae6d", // JUNGLE.TR2 + "21577b6e5828f1535ece0ca45b63ad5a", // CUT6.TR2 + "b7af59e80ae03e05202754764a8fb132", // TEMPLE.TR2 + "e4d88cb486c8054f7db6076ab9748792", // QUADCHAS.TR2 + "a0f0372b444a01676d99c7a0f49ad24e", // TONYBOSS.TR2 + "8080e2eb148541bc44a74317fbb93ad2", // SHORE.TR2 + "005183d2dd54f5fbb4ccf3293d643964", // CRASH.TR2 + "0cebb72fee215b40b722f0e96106a3a9", // RAPIDS.TR2 + "5c18e057ea40e4c1ceecebc5a9d9cd65", // TRIBOSS.TR2 + "633b0245e14c5b7faab90d1f549ac488", // ROOFS.TR2 + "864a61e73a371dfe0c593f8a233930ec", // SEWER.TR2 + "57a207f21c00362ce60f0ad3f5455a07", // TOWER.TR2 + "69a07074215c08a547a0055b774bacec", // OFFICE.TR2 + "1c4249c0fff242da45728f5177122c10", // STPAUL.TR2 + "62893b73166013b21e2fb3d9c8d9d606", // NEVADA.TR2 + "150b8eae81456cc7b928a40c1c9992ce", // COMPOUND.TR2 + "5e4d62be8333842ef61ac6720b8b77c3", // AREA51.TR2 + "42fd4ca5aa5c02bc4478bbd583689c1a", // ANTARC.TR2 + "d107f57cecfc03b53d00a46a0111dce5", // MINES.TR2 + "436620ba935ffc3cd798cf598ee46ae4", // CITY.TR2 + "ee94ce8722881ab7a1c66cda17bef933", // CHAMBER.TR2 + "1c4249c0fff242da45728f5177122c10", // STPAUL.TR2 + "6efcc71cace4527a0b79128111910eb7", // SCOTLAND.TR2 + "8dc5ff814b23b0654a0b261e501d0320", // WILLSDEN.TR2 + "b99c1af5f462344c267a2141b238fcb4", // CHUNNEL.TR2 + "5d002059f337f165066474a902796d0b", // UNDERSEA.TR2 + "ef8d013cf1fb6495943d9dd74ee09588", // ZOO.TR2 + "ac3b508ae772b3fbd4b16b83b829dfc6", // SLINC.TR2 + + // TR3R V1.1+ + "9a1f7162eb7e2b3a57a17e92e0edfbb6", // ANTARC.TR2 }; } diff --git a/TRRandomizerCore/Levels/TR1RCombinedLevel.cs b/TRRandomizerCore/Levels/TR1RCombinedLevel.cs new file mode 100644 index 000000000..db5aad15a --- /dev/null +++ b/TRRandomizerCore/Levels/TR1RCombinedLevel.cs @@ -0,0 +1,20 @@ +using TRGE.Core; +using TRLevelControl.Helpers; +using TRLevelControl.Model; + +namespace TRRandomizerCore.Levels; + +public class TR1RCombinedLevel +{ + public TR1Level Data { get; set; } + public TRRScriptedLevel Script { get; set; } + public string Checksum { get; set; } + public string Name => Script.LevelFileBaseName.ToUpper(); + public TR1RCombinedLevel CutSceneLevel { get; set; } + public TR1RCombinedLevel ParentLevel { get; set; } + public bool IsCutScene => ParentLevel != null; + public bool HasCutScene => Script.HasCutScene; + public int Sequence => IsCutScene ? ParentLevel.Sequence : Script.Sequence; + public bool Is(string levelFileName) => Script.Is(levelFileName); + public bool IsAssault => Is(TR1LevelNames.ASSAULT); +} diff --git a/TRRandomizerCore/Levels/TR2RCombinedLevel.cs b/TRRandomizerCore/Levels/TR2RCombinedLevel.cs new file mode 100644 index 000000000..b80eebfc0 --- /dev/null +++ b/TRRandomizerCore/Levels/TR2RCombinedLevel.cs @@ -0,0 +1,20 @@ +using TRGE.Core; +using TRLevelControl.Helpers; +using TRLevelControl.Model; + +namespace TRRandomizerCore.Levels; + +public class TR2RCombinedLevel +{ + public TR2Level Data { get; set; } + public TRRScriptedLevel Script { get; set; } + public string Checksum { get; set; } + public string Name => Script.LevelFileBaseName.ToUpper(); + public TR2RCombinedLevel CutSceneLevel { get; set; } + public TR2RCombinedLevel ParentLevel { get; set; } + public bool IsCutScene => ParentLevel != null; + public bool HasCutScene => Script.HasCutScene; + public int Sequence => IsCutScene ? ParentLevel.Sequence : Script.Sequence; + public bool Is(string levelFileName) => Script.Is(levelFileName); + public bool IsAssault => Is(TR2LevelNames.ASSAULT); +} diff --git a/TRRandomizerCore/Levels/TR3RCombinedLevel.cs b/TRRandomizerCore/Levels/TR3RCombinedLevel.cs new file mode 100644 index 000000000..f441d86e6 --- /dev/null +++ b/TRRandomizerCore/Levels/TR3RCombinedLevel.cs @@ -0,0 +1,20 @@ +using TRGE.Core; +using TRLevelControl.Helpers; +using TRLevelControl.Model; + +namespace TRRandomizerCore.Levels; + +public class TR3RCombinedLevel +{ + public TR3Level Data { get; set; } + public TRRScriptedLevel Script { get; set; } + public string Checksum { get; set; } + public string Name => Script.LevelFileBaseName.ToUpper(); + public TR3RCombinedLevel CutSceneLevel { get; set; } + public TR3RCombinedLevel ParentLevel { get; set; } + public bool IsCutScene => ParentLevel != null; + public bool HasCutScene => Script.HasCutScene; + public int Sequence => IsCutScene ? ParentLevel.Sequence : Script.Sequence; + public bool Is(string levelFileName) => Script.Is(levelFileName); + public bool IsAssault => Is(TR3LevelNames.ASSAULT); +} diff --git a/TRRandomizerCore/Processors/TR1/TR1RLevelProcessor.cs b/TRRandomizerCore/Processors/TR1/TR1RLevelProcessor.cs new file mode 100644 index 000000000..c4cecb6b5 --- /dev/null +++ b/TRRandomizerCore/Processors/TR1/TR1RLevelProcessor.cs @@ -0,0 +1,72 @@ +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Model; +using TRRandomizerCore.Levels; + +namespace TRRandomizerCore.Processors; + +public class TR1RLevelProcessor : AbstractLevelProcessor +{ + protected TR1LevelControl _control; + + public TR1RLevelProcessor() + { + _control = new(); + } + + protected override TR1RCombinedLevel LoadCombinedLevel(TRRScriptedLevel scriptedLevel) + { + TR1RCombinedLevel level = new() + { + Data = LoadLevelData(scriptedLevel.LevelFileBaseName), + Script = scriptedLevel, + Checksum = GetBackupChecksum(scriptedLevel.LevelFileBaseName) + }; + + if (scriptedLevel.HasCutScene) + { + level.CutSceneLevel = LoadCombinedLevel(scriptedLevel.CutSceneLevel as TRRScriptedLevel); + level.CutSceneLevel.ParentLevel = level; + } + + return level; + } + + public TR1Level LoadLevelData(string name) + { + lock (_controlLock) + { + string fullPath = Path.Combine(BasePath, name); + return _control.Read(fullPath); + } + } + + protected override void ReloadLevelData(TR1RCombinedLevel level) + { + level.Data = LoadLevelData(level.Name); + if (level.HasCutScene) + { + level.CutSceneLevel.Data = LoadLevelData(level.CutSceneLevel.Name); + } + } + + protected override void SaveLevel(TR1RCombinedLevel level) + { + SaveLevel(level.Data, level.Name); + if (level.HasCutScene) + { + SaveLevel(level.CutSceneLevel.Data, level.CutSceneLevel.Name); + } + + SaveScript(); + } + + public void SaveLevel(TR1Level level, string name) + { + lock (_controlLock) + { + string fullPath = Path.Combine(BasePath, name); + _control.Write(level, fullPath); + } + } +} diff --git a/TRRandomizerCore/Processors/TR2/TR2RLevelProcessor.cs b/TRRandomizerCore/Processors/TR2/TR2RLevelProcessor.cs new file mode 100644 index 000000000..b71db7fb1 --- /dev/null +++ b/TRRandomizerCore/Processors/TR2/TR2RLevelProcessor.cs @@ -0,0 +1,72 @@ +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Model; +using TRRandomizerCore.Levels; + +namespace TRRandomizerCore.Processors; + +public class TR2RLevelProcessor : AbstractLevelProcessor +{ + protected TR2LevelControl _control; + + public TR2RLevelProcessor() + { + _control = new(); + } + + protected override TR2RCombinedLevel LoadCombinedLevel(TRRScriptedLevel scriptedLevel) + { + TR2RCombinedLevel level = new() + { + Data = LoadLevelData(scriptedLevel.LevelFileBaseName), + Script = scriptedLevel, + Checksum = GetBackupChecksum(scriptedLevel.LevelFileBaseName) + }; + + if (scriptedLevel.HasCutScene) + { + level.CutSceneLevel = LoadCombinedLevel(scriptedLevel.CutSceneLevel as TRRScriptedLevel); + level.CutSceneLevel.ParentLevel = level; + } + + return level; + } + + public TR2Level LoadLevelData(string name) + { + lock (_controlLock) + { + string fullPath = Path.Combine(BasePath, name); + return _control.Read(fullPath); + } + } + + protected override void ReloadLevelData(TR2RCombinedLevel level) + { + level.Data = LoadLevelData(level.Name); + if (level.HasCutScene) + { + level.CutSceneLevel.Data = LoadLevelData(level.CutSceneLevel.Name); + } + } + + protected override void SaveLevel(TR2RCombinedLevel level) + { + SaveLevel(level.Data, level.Name); + if (level.HasCutScene) + { + SaveLevel(level.CutSceneLevel.Data, level.CutSceneLevel.Name); + } + + SaveScript(); + } + + public void SaveLevel(TR2Level level, string name) + { + lock (_controlLock) + { + string fullPath = Path.Combine(BasePath, name); + _control.Write(level, fullPath); + } + } +} diff --git a/TRRandomizerCore/Processors/TR3/TR3RLevelProcessor.cs b/TRRandomizerCore/Processors/TR3/TR3RLevelProcessor.cs new file mode 100644 index 000000000..d742ba0d1 --- /dev/null +++ b/TRRandomizerCore/Processors/TR3/TR3RLevelProcessor.cs @@ -0,0 +1,72 @@ +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Model; +using TRRandomizerCore.Levels; + +namespace TRRandomizerCore.Processors; + +public class TR3RLevelProcessor : AbstractLevelProcessor +{ + protected TR3LevelControl _control; + + public TR3RLevelProcessor() + { + _control = new(); + } + + protected override TR3RCombinedLevel LoadCombinedLevel(TRRScriptedLevel scriptedLevel) + { + TR3RCombinedLevel level = new() + { + Data = LoadLevelData(scriptedLevel.LevelFileBaseName), + Script = scriptedLevel, + Checksum = GetBackupChecksum(scriptedLevel.LevelFileBaseName) + }; + + if (scriptedLevel.HasCutScene) + { + level.CutSceneLevel = LoadCombinedLevel(scriptedLevel.CutSceneLevel as TRRScriptedLevel); + level.CutSceneLevel.ParentLevel = level; + } + + return level; + } + + public TR3Level LoadLevelData(string name) + { + lock (_controlLock) + { + string fullPath = Path.Combine(BasePath, name); + return _control.Read(fullPath); + } + } + + protected override void ReloadLevelData(TR3RCombinedLevel level) + { + level.Data = LoadLevelData(level.Name); + if (level.HasCutScene) + { + level.CutSceneLevel.Data = LoadLevelData(level.CutSceneLevel.Name); + } + } + + protected override void SaveLevel(TR3RCombinedLevel level) + { + SaveLevel(level.Data, level.Name); + if (level.HasCutScene) + { + SaveLevel(level.CutSceneLevel.Data, level.CutSceneLevel.Name); + } + + SaveScript(); + } + + public void SaveLevel(TR3Level level, string name) + { + lock (_controlLock) + { + string fullPath = Path.Combine(BasePath, name); + _control.Write(level, fullPath); + } + } +} diff --git a/TRRandomizerCore/Randomizers/Shared/LocationPicker.cs b/TRRandomizerCore/Randomizers/Shared/LocationPicker.cs index 7784bc6ea..f793f9de5 100644 --- a/TRRandomizerCore/Randomizers/Shared/LocationPicker.cs +++ b/TRRandomizerCore/Randomizers/Shared/LocationPicker.cs @@ -9,8 +9,6 @@ namespace TRRandomizerCore.Randomizers; public class LocationPicker : IRouteManager { - private static readonly int _rotation = -8192; - private readonly Dictionary> _routes; private List _locations, _usedTriggerLocations, _currentRoute; @@ -333,7 +331,7 @@ public void SetLocation(TREntity entity, Location location) // around randomly for variety. if (entity.Angle == -1) { - entity.Angle = (short)(_generator.Next(0, 8) * _rotation); + entity.Angle = (short)(_generator.Next(0, 8) * -TRConsts.Angle45); } } diff --git a/TRRandomizerCore/Randomizers/TR1/TR1StartPositionRandomizer.cs b/TRRandomizerCore/Randomizers/TR1/TR1StartPositionRandomizer.cs index d3eebaf1b..ccc329b8f 100644 --- a/TRRandomizerCore/Randomizers/TR1/TR1StartPositionRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR1/TR1StartPositionRandomizer.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using TRFDControl; using TRGE.Core; +using TRLevelControl; using TRLevelControl.Model; using TRRandomizerCore.Helpers; using TRRandomizerCore.Levels; @@ -10,7 +11,6 @@ namespace TRRandomizerCore.Randomizers; public class TR1StartPositionRandomizer : BaseTR1Randomizer { - private static readonly short _rotation = -8192; private Dictionary> _startLocations; public override void Randomize(int seed) @@ -21,9 +21,7 @@ public override void Randomize(int seed) foreach (TR1ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeStartPosition(_levelInstance); - SaveLevelInstance(); if (!TriggerProgress()) @@ -35,7 +33,7 @@ public override void Randomize(int seed) private void RandomizeStartPosition(TR1CombinedLevel level) { - TR1Entity lara = level.Data.Entities.Find(e => e.TypeID == (short)TR1Type.Lara); + TR1Entity lara = level.Data.Entities.Find(e => e.TypeID == TR1Type.Lara); FDControl floorData = new(); floorData.ParseFromLevel(level.Data); @@ -45,45 +43,24 @@ private void RandomizeStartPosition(TR1CombinedLevel level) if (!Settings.RotateStartPositionOnly && _startLocations.ContainsKey(level.Name)) { List locations = _startLocations[level.Name]; - if (Settings.DevelopmentMode) + Location location; + do { - foreach (Location loc in locations) - { - level.Data.Entities.Add(new() - { - TypeID = (short)TR1Type.Lara, - X = loc.X, - Y = loc.Y, - Z = loc.Z, - Room = (short)loc.Room, - Angle = lara.Angle - }); - } + location = locations[_generator.Next(0, locations.Count)]; } - else - { - Location location; - do - { - location = locations[_generator.Next(0, locations.Count)]; - } - while (!location.Validated || location.ContainsSecret(level.Data, floorData)); + while (!location.Validated || location.ContainsSecret(level.Data, floorData)); - lara.X = location.X; - lara.Y = location.Y; - lara.Z = location.Z; - lara.Room = (short)location.Room; - lara.Angle = (short)(_generator.Next(0, 8) * _rotation); - } + lara.X = location.X; + lara.Y = location.Y; + lara.Z = location.Z; + lara.Room = (short)location.Room; } - else + + short currentAngle = lara.Angle; + do { - short currentAngle = lara.Angle; - do - { - lara.Angle = (short)(_generator.Next(0, 8) * _rotation); - } - while (lara.Angle == currentAngle); + lara.Angle = (short)(_generator.Next(0, 8) * -TRConsts.Angle45); } + while (lara.Angle == currentAngle); } } diff --git a/TRRandomizerCore/Randomizers/TR1R/BaseTR1RRandomizer.cs b/TRRandomizerCore/Randomizers/TR1R/BaseTR1RRandomizer.cs new file mode 100644 index 000000000..b04f72328 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR1R/BaseTR1RRandomizer.cs @@ -0,0 +1,13 @@ +using TRRandomizerCore.Editors; +using TRRandomizerCore.Processors; + +namespace TRRandomizerCore.Randomizers; + +public abstract class BaseTR1RRandomizer : TR1RLevelProcessor, IRandomizer +{ + public RandomizerSettings Settings { get; internal set; } + + protected Random _generator; + + public abstract void Randomize(int seed); +} diff --git a/TRRandomizerCore/Randomizers/TR1R/TR1RAudioRandomizer.cs b/TRRandomizerCore/Randomizers/TR1R/TR1RAudioRandomizer.cs new file mode 100644 index 000000000..dc0bf1501 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR1R/TR1RAudioRandomizer.cs @@ -0,0 +1,119 @@ +using System.Numerics; +using TREnvironmentEditor.Model.Types; +using TRFDControl; +using TRFDControl.FDEntryTypes; +using TRFDControl.Utilities; +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Model; +using TRRandomizerCore.Levels; + +namespace TRRandomizerCore.Randomizers; + +public class TR1RAudioRandomizer : BaseTR1RRandomizer +{ + private const int _defaultSecretTrack = 13; + + private AudioRandomizer _audioRandomizer; + + public override void Randomize(int seed) + { + _generator = new(seed); + + LoadAudioData(); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + + RandomizeMusicTriggers(_levelInstance); + RandomizeWibble(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + private void LoadAudioData() + { + _audioRandomizer = new(ScriptEditor.AudioProvider.GetCategorisedTracks()); + } + + private void RandomizeMusicTriggers(TR1RCombinedLevel level) + { + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + if (Settings.ChangeTriggerTracks) + { + RandomizeFloorTracks(level.Data, floorData); + } + + if (Settings.SeparateSecretTracks) + { + RandomizeSecretTracks(level, floorData); + } + + floorData.WriteToLevel(level.Data); + } + + private void RandomizeFloorTracks(TR1Level level, FDControl floorData) + { + _audioRandomizer.ResetFloorMap(); + foreach (TRRoom room in level.Rooms.Where(r => (r.Flags & EMLockMusicFunction.LockedMusicFlag) == 0)) + { + _audioRandomizer.RandomizeFloorTracks(room.Sectors, 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(TR1RCombinedLevel level, FDControl floorData) + { + 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; + } + + FDActionListItem musicAction = new() + { + TrigAction = FDTrigAction.PlaySoundtrack, + Parameter = secretTrack.ID + }; + + List triggers = FDUtilities.GetSecretTriggers(floorData, i); + foreach (FDTriggerEntry trigger in triggers) + { + FDActionListItem currentMusicAction = trigger.TrigActionList.Find(a => a.TrigAction == FDTrigAction.PlaySoundtrack); + if (currentMusicAction == null) + { + trigger.TrigActionList.Add(musicAction); + } + } + } + } + + private void RandomizeWibble(TR1RCombinedLevel level) + { + if (Settings.RandomizeWibble) + { + foreach (TRSoundDetails details in level.Data.SoundDetails) + { + details.Wibble = true; + } + } + } +} diff --git a/TRRandomizerCore/Randomizers/TR1R/TR1REnvironmentRandomizer.cs b/TRRandomizerCore/Randomizers/TR1R/TR1REnvironmentRandomizer.cs new file mode 100644 index 000000000..0180ce550 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR1R/TR1REnvironmentRandomizer.cs @@ -0,0 +1,84 @@ +using TREnvironmentEditor; +using TREnvironmentEditor.Helpers; +using TREnvironmentEditor.Model; +using TRGE.Core; +using TRRandomizerCore.Levels; + +namespace TRRandomizerCore.Randomizers; + +public class TR1REnvironmentRandomizer : BaseTR1RRandomizer +{ + public override void Randomize(int seed) + { + _generator ??= new(seed); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + RandomizeEnvironment(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + public void FinalizeEnvironment() + { + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + FinalizeEnvironment(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + private EMEditorMapping GetMapping(TR1RCombinedLevel level) + => EMEditorMapping.Get(GetResourcePath($@"TR1\Environment\{level.Name}-Environment.json")); + + private void RandomizeEnvironment(TR1RCombinedLevel level) + { + EMEditorMapping mapping = GetMapping(level); + if (mapping != null) + { + ApplyMappingToLevel(level, mapping); + } + } + + private void ApplyMappingToLevel(TR1RCombinedLevel level, EMEditorMapping mapping) + { + EnvironmentPicker picker = new(Settings.HardEnvironmentMode) + { + Generator = _generator + }; + picker.LoadTags(Settings, true); + picker.Options.ExclusionMode = EMExclusionMode.Individual; + + mapping.All.ApplyToLevel(level.Data, picker.Options); + + // No further mods supported yet + } + + private void FinalizeEnvironment(TR1RCombinedLevel level) + { + EMEditorMapping mapping = GetMapping(level); + EnvironmentPicker picker = new(Settings.HardEnvironmentMode); + picker.Options.ExclusionMode = EMExclusionMode.Individual; + picker.ResetTags(true); + + if (mapping != null) + { + foreach (EMConditionalSingleEditorSet mod in mapping.ConditionalAll) + { + mod.ApplyToLevel(level.Data, picker.Options); + } + } + } +} diff --git a/TRRandomizerCore/Randomizers/TR1R/TR1RItemRandomizer.cs b/TRRandomizerCore/Randomizers/TR1R/TR1RItemRandomizer.cs new file mode 100644 index 000000000..058d40697 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR1R/TR1RItemRandomizer.cs @@ -0,0 +1,264 @@ +using Newtonsoft.Json; +using TRFDControl; +using TRFDControl.Utilities; +using TRGE.Core; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; +using TRRandomizerCore.Secrets; +using TRRandomizerCore.Utilities; + +namespace TRRandomizerCore.Randomizers; + +public class TR1RItemRandomizer : BaseTR1RRandomizer +{ + private readonly Dictionary> _excludedLocations, _pistolLocations; + private readonly LocationPicker _picker; + + private TRSecretMapping _secretMapping; + private TR1Entity _unarmedLevelPistols; + + public ItemFactory ItemFactory { get; set; } + + public TR1RItemRandomizer() + { + _excludedLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR1\Locations\invalid_item_locations.json")); + _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR1\Locations\unarmed_locations.json")); + _picker = new(GetResourcePath(@"TR1\Locations\routes.json")); + } + + public override void Randomize(int seed) + { + _generator = new(seed); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + FindUnarmedLevelPistols(_levelInstance); + + _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, false), Settings, _generator); + _secretMapping = TRSecretMapping.Get(GetResourcePath($@"TR1\SecretMapping\{_levelInstance.Name}-SecretMapping.json")); + + if (Settings.RandomizeItemTypes) + { + RandomizeItemTypes(_levelInstance); + } + + if (Settings.RandomizeItemPositions) + { + RandomizeItemLocations(_levelInstance); + } + + if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) + { + EnforceOneLimit(_levelInstance); + } + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + public void RandomizeKeyItems() + { + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + RandomizeKeyItems(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + private void FindUnarmedLevelPistols(TR1RCombinedLevel level) + { + if (level.Script.RemovesWeapons) + { + _unarmedLevelPistols = level.Data.Entities.Find( + e => TR1TypeUtilities.IsWeaponPickup(e.TypeID) + && _pistolLocations[level.Name].Any(l => l.IsEquivalent(e.GetLocation()))); + } + else + { + _unarmedLevelPistols = null; + } + } + + private List GetItemLocationPool(TR1RCombinedLevel level, bool keyItemMode) + { + List exclusions = new(); + if (_excludedLocations.ContainsKey(level.Name)) + { + exclusions.AddRange(_excludedLocations[level.Name]); + } + + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + foreach (TR1Entity entity in level.Data.Entities) + { + if (!TR1TypeUtilities.CanSharePickupSpace(entity.TypeID)) + { + exclusions.Add(entity.GetFloorLocation(loc => + FDUtilities.GetRoomSector(loc.X, loc.Y, loc.Z, (short)loc.Room, level.Data, floorData))); + } + } + + TR1LocationGenerator generator = new(); + return generator.Generate(level.Data, exclusions, keyItemMode); + } + + public void RandomizeItemTypes(TR1RCombinedLevel level) + { + if (level.IsAssault) + { + return; + } + + List stdItemTypes = TR1TypeUtilities.GetStandardPickupTypes(); + stdItemTypes.Remove(TR1Type.PistolAmmo_S_P); + + bool hasPistols = level.Data.Entities.Any(e => e.TypeID == TR1Type.Pistols_S_P); + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR1Entity entity = level.Data.Entities[i]; + TR1Type entityType = entity.TypeID; + if (!TR1TypeUtilities.IsStandardPickupType(entityType) || _secretMapping.RewardEntities.Contains(i)) + { + continue; + } + + if (entity == _unarmedLevelPistols) + { + if (entityType == TR1Type.Pistols_S_P && Settings.GiveUnarmedItems) + { + do + { + entityType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; + } + while (!TR1TypeUtilities.IsWeaponPickup(entityType)); + entity.TypeID = entityType; + } + } + else if (TR1TypeUtilities.IsStandardPickupType(entityType)) + { + TR1Type newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; + if (newType == TR1Type.Pistols_S_P && (hasPistols || !level.Script.RemovesWeapons)) + { + do + { + newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; + } + while (!TR1TypeUtilities.IsWeaponPickup(newType) || newType == TR1Type.Pistols_S_P); + } + entity.TypeID = newType; + } + + hasPistols = level.Data.Entities.Any(e => e.TypeID == TR1Type.Pistols_S_P); + } + } + + public void RandomizeItemLocations(TR1RCombinedLevel level) + { + if (level.IsAssault) + { + return; + } + + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR1Entity entity = level.Data.Entities[i]; + if (!TR1TypeUtilities.IsStandardPickupType(entity.TypeID) + || _secretMapping.RewardEntities.Contains(i) + || ItemFactory.IsItemLocked(level.Name, i) + || entity == _unarmedLevelPistols) + { + continue; + } + + _picker.RandomizePickupLocation(entity); + entity.Intensity = 0; + } + } + + public void EnforceOneLimit(TR1RCombinedLevel level) + { + if (level.IsAssault) + { + return; + } + + HashSet uniqueTypes = new(); + if (_unarmedLevelPistols != null) + { + uniqueTypes.Add(_unarmedLevelPistols.TypeID); + } + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR1Entity entity = level.Data.Entities[i]; + if (_secretMapping.RewardEntities.Contains(i) || entity == _unarmedLevelPistols) + { + continue; + } + + if ((TR1TypeUtilities.IsStandardPickupType(entity.TypeID) || TR1TypeUtilities.IsCrystalPickup(entity.TypeID)) + && !uniqueTypes.Add(entity.TypeID)) + { + ItemUtilities.HideEntity(entity); + ItemFactory.FreeItem(level.Name, i); + } + } + } + + private void RandomizeKeyItems(TR1RCombinedLevel level) + { + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level.Data, floorData); + _picker.RoomInfos = level.Data.Rooms + .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) + .ToList(); + + _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, true), Settings, _generator); + + if (level.Is(TR1LevelNames.TIHOCAN) + && level.Data.Entities[TR1ItemRandomizer.TihocanPierreIndex].TypeID != TR1Type.Pierre) + { + level.Data.Entities.AddRange(TR1ItemRandomizer.TihocanEndItems); + } + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR1Entity entity = level.Data.Entities[i]; + if (!IsMovableKeyItem(level, entity) + || ItemFactory.IsItemLocked(level.Name, i)) + { + continue; + } + + bool hasPickupTrigger = LocationUtilities.HasPickupTriger(entity, i, level.Data, floorData); + _picker.RandomizeKeyItemLocation(entity, hasPickupTrigger, + level.Script.OriginalSequence, level.Data.Rooms[entity.Room].Info); + } + } + + private static bool IsMovableKeyItem(TR1RCombinedLevel level, TR1Entity entity) + { + return TR1TypeUtilities.IsKeyItemType(entity.TypeID) + || (level.Is(TR1LevelNames.TIHOCAN) && entity.TypeID == TR1Type.ScionPiece2_S_P); + } +} diff --git a/TRRandomizerCore/Randomizers/TR1R/TR1RStartPositionRandomizer.cs b/TRRandomizerCore/Randomizers/TR1R/TR1RStartPositionRandomizer.cs new file mode 100644 index 000000000..13872bc96 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR1R/TR1RStartPositionRandomizer.cs @@ -0,0 +1,64 @@ +using Newtonsoft.Json; +using TRFDControl; +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; +using TRRandomizerCore.Utilities; + +namespace TRRandomizerCore.Randomizers; + +public class TR1RStartPositionRandomizer : BaseTR1RRandomizer +{ + private Dictionary> _startLocations; + + public override void Randomize(int seed) + { + _generator = new Random(seed); + _startLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR1\Locations\start_positions.json")); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + RandomizeStartPosition(_levelInstance); + SaveLevelInstance(); + + if (!TriggerProgress()) + { + break; + } + } + } + + private void RandomizeStartPosition(TR1RCombinedLevel level) + { + TR1Entity lara = level.Data.Entities.Find(e => e.TypeID == TR1Type.Lara); + + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + if (!Settings.RotateStartPositionOnly && _startLocations.ContainsKey(level.Name)) + { + List locations = _startLocations[level.Name]; + Location location; + do + { + location = locations[_generator.Next(0, locations.Count)]; + } + while (!location.Validated || location.ContainsSecret(level.Data, floorData)); + + lara.X = location.X; + lara.Y = location.Y; + lara.Z = location.Z; + lara.Room = (short)location.Room; + } + + short currentAngle = lara.Angle; + do + { + lara.Angle = (short)(_generator.Next(0, 8) * -TRConsts.Angle45); + } + while (lara.Angle == currentAngle); + } +} diff --git a/TRRandomizerCore/Randomizers/TR2/TR2StartPositionRandomizer.cs b/TRRandomizerCore/Randomizers/TR2/TR2StartPositionRandomizer.cs index 39a276fce..b15dd95bc 100644 --- a/TRRandomizerCore/Randomizers/TR2/TR2StartPositionRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR2/TR2StartPositionRandomizer.cs @@ -4,6 +4,7 @@ using TRGE.Core; using TRLevelControl.Helpers; using TRLevelControl.Model; +using TRLevelControl; namespace TRRandomizerCore.Randomizers; @@ -19,9 +20,7 @@ public override void Randomize(int seed) foreach (TR2ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeStartPosition(_levelInstance); - SaveLevelInstance(); if (!TriggerProgress()) @@ -40,7 +39,7 @@ private void RandomizeStartPosition(TR2CombinedLevel level) return; } - TR2Entity lara = level.Data.Entities.Find(e => e.TypeID == (short)TR2Type.Lara); + TR2Entity lara = level.Data.Entities.Find(e => e.TypeID == TR2Type.Lara); // We only change position if there is not a secret in the same room as Lara, This is just in case it ends up // where she starts on a slope (GW or Opera House for example), as its X,Y,Z values may not be identical to Lara's, @@ -57,45 +56,18 @@ private void RandomizeStartPosition(TR2CombinedLevel level) if (!Settings.RotateStartPositionOnly && _startLocations.ContainsKey(level.Name)) { List locations = _startLocations[level.Name]; - if (Settings.DevelopmentMode) - { - foreach (Location loc in locations) - { - level.Data.Entities.Add(new() - { - TypeID = (short)TR2Type.Lara, - Room = Convert.ToInt16(loc.Room), - X = loc.X, - Y = loc.Y, - Z = loc.Z, - Angle = 0, - Intensity1 = -1, - Intensity2 = -1, - Flags = 0 - }); - } - } - else - { - Location location = locations[_generator.Next(0, locations.Count)]; - lara.Room = (short)location.Room; - lara.X = location.X; - lara.Y = location.Y; - lara.Z = location.Z; - lara.Angle = location.Angle; - } + Location location = locations[_generator.Next(0, locations.Count)]; + lara.Room = (short)location.Room; + lara.X = location.X; + lara.Y = location.Y; + lara.Z = location.Z; + lara.Angle = location.Angle; } - RotateLara(lara, level); - } - - private void RotateLara(TR2Entity lara, TR2CombinedLevel level) - { short currentAngle = lara.Angle; do { - int degrees = 45 * _generator.Next(0, 8); - lara.Angle = (short)(degrees * 16384 / -90); + lara.Angle = (short)(_generator.Next(0, 8) * -TRConsts.Angle45); } while (lara.Angle == currentAngle); diff --git a/TRRandomizerCore/Randomizers/TR2R/BaseTR2RRandomizer.cs b/TRRandomizerCore/Randomizers/TR2R/BaseTR2RRandomizer.cs new file mode 100644 index 000000000..ca2d48525 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR2R/BaseTR2RRandomizer.cs @@ -0,0 +1,13 @@ +using TRRandomizerCore.Editors; +using TRRandomizerCore.Processors; + +namespace TRRandomizerCore.Randomizers; + +public abstract class BaseTR2RRandomizer : TR2RLevelProcessor, IRandomizer +{ + public RandomizerSettings Settings { get; internal set; } + + protected Random _generator; + + public abstract void Randomize(int seed); +} diff --git a/TRRandomizerCore/Randomizers/TR2R/TR2RAudioRandomizer.cs b/TRRandomizerCore/Randomizers/TR2R/TR2RAudioRandomizer.cs new file mode 100644 index 000000000..ae13be06b --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR2R/TR2RAudioRandomizer.cs @@ -0,0 +1,290 @@ +using Newtonsoft.Json; +using System.Numerics; +using TRFDControl; +using TRFDControl.FDEntryTypes; +using TRFDControl.Utilities; +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRModelTransporter.Handlers; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; +using TRRandomizerCore.SFX; + +namespace TRRandomizerCore.Randomizers; + +public class TR2RAudioRandomizer : BaseTR2RRandomizer +{ + private const int _maxSample = 407; + + private AudioRandomizer _audioRandomizer; + + private List> _soundEffects; + private List _sfxCategories; + private List _uncontrolledLevels; + + public override void Randomize(int seed) + { + _generator = new Random(seed); + + LoadAudioData(); + ChooseUncontrolledLevels(); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + + RandomizeMusicTriggers(_levelInstance); + RandomizeSoundEffects(_levelInstance); + RandomizeWibble(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + 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) + { + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + if (Settings.ChangeTriggerTracks) + { + RandomizeFloorTracks(level.Data, floorData); + } + + if (Settings.SeparateSecretTracks) + { + RandomizeSecretTracks(level.Data, floorData); + } + + floorData.WriteToLevel(level.Data); + } + + private void RandomizeFloorTracks(TR2Level level, FDControl floorData) + { + _audioRandomizer.ResetFloorMap(); + foreach (TR2Room room in level.Rooms) + { + _audioRandomizer.RandomizeFloorTracks(room.SectorList, 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, FDControl floorData) + { + List secretTracks = _audioRandomizer.GetTracks(TRAudioCategory.Secret); + Dictionary secrets = GetSecretItems(level); + foreach (int entityIndex in secrets.Keys) + { + TR2Entity secret = secrets[entityIndex]; + TRRoomSector sector = FDUtilities.GetRoomSector(secret.X, secret.Y, secret.Z, secret.Room, level, floorData); + if (sector.FDIndex == 0) + { + floorData.CreateFloorData(sector); + } + + List entries = floorData.Entries[sector.FDIndex]; + FDTriggerEntry existingTriggerEntry = entries.Find(e => e is FDTriggerEntry) as FDTriggerEntry; + bool existingEntityPickup = false; + if (existingTriggerEntry != null) + { + if (existingTriggerEntry.TrigType == FDTrigType.Pickup && existingTriggerEntry.TrigActionList[0].Parameter == entityIndex) + { + existingEntityPickup = true; + } + else + { + continue; + } + } + + FDActionListItem musicAction = new() + { + TrigAction = FDTrigAction.PlaySoundtrack, + Parameter = secretTracks[_generator.Next(0, secretTracks.Count)].ID + }; + + if (existingEntityPickup) + { + existingTriggerEntry.TrigActionList.Add(musicAction); + } + else + { + entries.Add(new FDTriggerEntry + { + Setup = new FDSetup { Value = 1028 }, + TrigSetup = new FDTrigSetup { Value = 15872 }, + TrigActionList = new List + { + new() { + TrigAction = FDTrigAction.Object, + Parameter = (ushort)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")); + } + } + + private void RandomizeSoundEffects(TR2RCombinedLevel level) + { + if (_sfxCategories.Count == 0) + { + return; + } + + if (IsUncontrolledLevel(level.Script)) + { + HashSet indices = new(); + while (indices.Count < level.Data.NumSampleIndices) + { + indices.Add((uint)_generator.Next(0, _maxSample + 1)); + } + level.Data.SampleIndices = indices.ToArray(); + } + else + { + for (int internalIndex = 0; internalIndex < level.Data.SoundMap.Length; internalIndex++) + { + TRSFXDefinition definition = _soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); + if (level.Data.SoundMap[internalIndex] == -1 || 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) + { + TRSFXDefinition nextDefinition = otherDefinitions[_generator.Next(0, otherDefinitions.Count)]; + short soundDetailsIndex = ImportSoundEffect(level.Data, nextDefinition); + if (soundDetailsIndex != -1) + { + level.Data.SoundMap[internalIndex] = soundDetailsIndex; + } + } + } + } + + SoundUtilities.ResortSoundIndices(level.Data); + } + + private static short ImportSoundEffect(TR2Level level, TRSFXDefinition definition) + { + if (definition.SampleIndices.Count == 0) + { + return -1; + } + + List levelSamples = level.SampleIndices.ToList(); + List levelSoundDetails = level.SoundDetails.ToList(); + + uint minSample = definition.SampleIndices.Min(); + if (levelSamples.Contains(minSample)) + { + return (short)levelSoundDetails.FindIndex(d => levelSamples[d.Sample] == minSample); + } + + ushort newSampleIndex = (ushort)levelSamples.Count; + List sortedSamples = new(definition.SampleIndices); + sortedSamples.Sort(); + levelSamples.AddRange(sortedSamples); + + level.SampleIndices = levelSamples.ToArray(); + level.NumSampleIndices = (uint)levelSamples.Count; + + levelSoundDetails.Add(new TRSoundDetails + { + Chance = definition.Details.Chance, + Characteristics = definition.Details.Characteristics, + Sample = newSampleIndex, + Volume = definition.Details.Volume + }); + + level.SoundDetails = levelSoundDetails.ToArray(); + level.NumSoundDetails++; + + return (short)(level.NumSoundDetails - 1); + } + + private void RandomizeWibble(TR2RCombinedLevel level) + { + if (Settings.RandomizeWibble) + { + foreach (TRSoundDetails details in level.Data.SoundDetails) + { + details.Wibble = true; + } + } + } +} diff --git a/TRRandomizerCore/Randomizers/TR2R/TR2REnvironmentRandomizer.cs b/TRRandomizerCore/Randomizers/TR2R/TR2REnvironmentRandomizer.cs new file mode 100644 index 000000000..6e5d1c650 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR2R/TR2REnvironmentRandomizer.cs @@ -0,0 +1,84 @@ +using TREnvironmentEditor; +using TREnvironmentEditor.Helpers; +using TREnvironmentEditor.Model; +using TRGE.Core; +using TRRandomizerCore.Levels; + +namespace TRRandomizerCore.Randomizers; + +public class TR2REnvironmentRandomizer : BaseTR2RRandomizer +{ + public override void Randomize(int seed) + { + _generator ??= new(seed); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + RandomizeEnvironment(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + public void FinalizeEnvironment() + { + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + FinalizeEnvironment(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + private EMEditorMapping GetMapping(TR2RCombinedLevel level) + => EMEditorMapping.Get(GetResourcePath($@"TR2\Environment\{level.Name}-Environment.json")); + + private void RandomizeEnvironment(TR2RCombinedLevel level) + { + EMEditorMapping mapping = GetMapping(level); + if (mapping != null) + { + ApplyMappingToLevel(level, mapping); + } + } + + private void ApplyMappingToLevel(TR2RCombinedLevel level, EMEditorMapping mapping) + { + EnvironmentPicker picker = new(Settings.HardEnvironmentMode) + { + Generator = _generator + }; + picker.LoadTags(Settings, true); + picker.Options.ExclusionMode = EMExclusionMode.Individual; + + mapping.All.ApplyToLevel(level.Data, picker.Options); + + // No further mods supported yet + } + + private void FinalizeEnvironment(TR2RCombinedLevel level) + { + EMEditorMapping mapping = GetMapping(level); + EnvironmentPicker picker = new(Settings.HardEnvironmentMode); + picker.Options.ExclusionMode = EMExclusionMode.Individual; + picker.ResetTags(true); + + if (mapping != null) + { + foreach (EMConditionalSingleEditorSet mod in mapping.ConditionalAll) + { + mod.ApplyToLevel(level.Data, picker.Options); + } + } + } +} diff --git a/TRRandomizerCore/Randomizers/TR2R/TR2RItemRandomizer.cs b/TRRandomizerCore/Randomizers/TR2R/TR2RItemRandomizer.cs new file mode 100644 index 000000000..48e66ec16 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR2R/TR2RItemRandomizer.cs @@ -0,0 +1,229 @@ +using Newtonsoft.Json; +using TRFDControl; +using TRFDControl.Utilities; +using TRGE.Core; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; +using TRRandomizerCore.Utilities; + +namespace TRRandomizerCore.Randomizers; + +public class TR2RItemRandomizer : BaseTR2RRandomizer +{ + private readonly Dictionary> _excludedLocations, _pistolLocations; + private readonly LocationPicker _picker; + + private TR2Entity _unarmedLevelPistols; + + public ItemFactory ItemFactory { get; set; } + + public TR2RItemRandomizer() + { + _excludedLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR2\Locations\invalid_item_locations.json")); + _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR2\Locations\unarmed_locations.json")); + _picker = new(GetResourcePath(@"TR2\Locations\routes.json")); + } + + public override void Randomize(int seed) + { + _generator = new(seed); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + FindUnarmedLevelPistols(_levelInstance); + + _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, false), Settings, _generator); + + if (Settings.RandomizeItemTypes) + { + RandomizeItemTypes(_levelInstance); + } + + if (Settings.RandomizeItemPositions) + { + RandomizeItemLocations(_levelInstance); + } + + if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) + { + EnforceOneLimit(_levelInstance); + } + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + public void RandomizeKeyItems() + { + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + RandomizeKeyItems(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + private void FindUnarmedLevelPistols(TR2RCombinedLevel level) + { + if (level.Script.RemovesWeapons) + { + _unarmedLevelPistols = level.Data.Entities.Find( + e => (TR2TypeUtilities.IsGunType(e.TypeID) || e.TypeID == TR2Type.Pistols_S_P) + && _pistolLocations[level.Name].Any(l => l.IsEquivalent(e.GetLocation()))); + } + else + { + _unarmedLevelPistols = null; + } + } + + private List GetItemLocationPool(TR2RCombinedLevel level, bool keyItemMode) + { + List exclusions = new(); + if (_excludedLocations.ContainsKey(level.Name)) + { + exclusions.AddRange(_excludedLocations[level.Name]); + } + + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + foreach (TR2Entity entity in level.Data.Entities) + { + if (!TR2TypeUtilities.CanSharePickupSpace(entity.TypeID)) + { + exclusions.Add(entity.GetFloorLocation(loc => + FDUtilities.GetRoomSector(loc.X, loc.Y, loc.Z, (short)loc.Room, level.Data, floorData))); + } + } + + TR2LocationGenerator generator = new(); + return generator.Generate(level.Data, exclusions, keyItemMode); + } + + private void RandomizeItemTypes(TR2RCombinedLevel level) + { + if (level.IsAssault + || (level.Is(TR2LevelNames.HOME) && (level.Script.RemovesWeapons || level.Script.RemovesAmmo))) + { + return; + } + + List stdItemTypes = TR2TypeUtilities.GetStandardPickupTypes(); + IEnumerable pickups = level.Data.Entities.Where(e => e != _unarmedLevelPistols && stdItemTypes.Contains(e.TypeID)); + + foreach (TR2Entity entity in pickups) + { + entity.TypeID = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; + } + } + + public void RandomizeItemLocations(TR2RCombinedLevel level) + { + if (level.Is(TR2LevelNames.HOME) && (level.Script.RemovesWeapons || level.Script.RemovesAmmo)) + { + return; + } + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR2Entity entity = level.Data.Entities[i]; + if (!TR2TypeUtilities.IsStandardPickupType(entity.TypeID) + || ItemFactory.IsItemLocked(level.Name, i) + || entity == _unarmedLevelPistols) + { + continue; + } + + _picker.RandomizePickupLocation(entity); + entity.Intensity1 = entity.Intensity2 = -1; + } + } + + private void EnforceOneLimit(TR2RCombinedLevel level) + { + HashSet uniqueTypes = new(); + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR2Entity entity = level.Data.Entities[i]; + if (TR2TypeUtilities.IsStandardPickupType(entity.TypeID) + && !uniqueTypes.Add(entity.TypeID)) + { + ItemUtilities.HideEntity(entity); + ItemFactory.FreeItem(level.Name, i); + } + } + } + + private void RandomizeKeyItems(TR2RCombinedLevel level) + { + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level.Data, floorData); + _picker.KeyItemTestAction = (location, hasPickupTrigger) => TestKeyItemLocation(location, hasPickupTrigger, level); + _picker.RoomInfos = level.Data.Rooms + .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) + .ToList(); + + _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, true), Settings, _generator); + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR2Entity entity = level.Data.Entities[i]; + if (!TR2TypeUtilities.IsKeyItemType(entity.TypeID) + || ItemFactory.IsItemLocked(level.Name, i)) + { + continue; + } + + // In OG, all puzzle2 items are switched to puzzle3 to allow the dragon to be imported everywhere. + // This means routes have been defined to look for these types, so we need to flip them temporarily. + // See TR2ModelAdjuster and LocationPicker.GetKeyItemID. + bool flipPuzzle2 = entity.TypeID == TR2Type.Puzzle2_S_P; + if (flipPuzzle2) + { + entity.TypeID = TR2Type.Puzzle3_S_P; + } + + _picker.RandomizeKeyItemLocation( + entity, LocationUtilities.HasPickupTriger(entity, i, level.Data, floorData), + level.Script.OriginalSequence, level.Data.Rooms[entity.Room].Info); + entity.Intensity1 = entity.Intensity2 = -1; + + if (flipPuzzle2) + { + entity.TypeID = TR2Type.Puzzle2_S_P; + } + } + } + + private bool TestKeyItemLocation(Location location, bool hasPickupTrigger, TR2RCombinedLevel level) + { + // Make sure if we're placing on the same tile as an enemy, that the + // enemy can drop the item. + TR2Entity enemy = level.Data.Entities + .FindAll(e => TR2TypeUtilities.IsEnemyType(e.TypeID)) + .Find(e => e.GetLocation().IsEquivalent(location)); + + return enemy == null || (Settings.AllowEnemyKeyDrops && !hasPickupTrigger && TR2TypeUtilities.CanDropPickups + ( + TR2TypeUtilities.GetAliasForLevel(level.Name, enemy.TypeID), + Settings.RandomizeEnemies && !Settings.ProtectMonks, + Settings.RandomizeEnemies && Settings.UnconditionalChickens + )); + } +} diff --git a/TRRandomizerCore/Randomizers/TR2R/TR2RStartPositionRandomizer.cs b/TRRandomizerCore/Randomizers/TR2R/TR2RStartPositionRandomizer.cs new file mode 100644 index 000000000..ce60423db --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR2R/TR2RStartPositionRandomizer.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; +using TRGE.Core; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRLevelControl; + +namespace TRRandomizerCore.Randomizers; + +public class TR2RStartPositionRandomizer : BaseTR2RRandomizer +{ + private Dictionary> _startLocations; + + public override void Randomize(int seed) + { + _generator = new(seed); + _startLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR2\Locations\start_positions.json")); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + RandomizeStartPosition(_levelInstance); + SaveLevelInstance(); + + if (!TriggerProgress()) + { + break; + } + } + } + + private void RandomizeStartPosition(TR2RCombinedLevel level) + { + if (level.Script.HasStartAnimation) + { + return; + } + + TR2Entity lara = level.Data.Entities.Find(e => e.TypeID == TR2Type.Lara); + + if (level.Data.Entities.Find(e => e.Room == lara.Room + && TR2TypeUtilities.IsSecretType(e.TypeID)) != null) + { + return; + } + + if (!Settings.RotateStartPositionOnly && _startLocations.ContainsKey(level.Name)) + { + List locations = _startLocations[level.Name]; + Location location = locations[_generator.Next(0, locations.Count)]; + lara.Room = (short)location.Room; + lara.X = location.X; + lara.Y = location.Y; + lara.Z = location.Z; + lara.Angle = location.Angle; + } + + short currentAngle = lara.Angle; + do + { + lara.Angle = (short)(_generator.Next(0, 8) * -TRConsts.Angle45); + } + while (lara.Angle == currentAngle); + + if (level.Is(TR2LevelNames.BARTOLI)) + { + TR2Entity boat = level.Data.Entities.Find(e => e.TypeID == TR2Type.Boat); + boat.Angle = lara.Angle; + } + } +} diff --git a/TRRandomizerCore/Randomizers/TR3/TR3StartPositionRandomizer.cs b/TRRandomizerCore/Randomizers/TR3/TR3StartPositionRandomizer.cs index 7c32d31b7..0ecf4d18d 100644 --- a/TRRandomizerCore/Randomizers/TR3/TR3StartPositionRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR3/TR3StartPositionRandomizer.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; -using TREnvironmentEditor.Helpers; using TREnvironmentEditor.Model.Types; using TRGE.Core; +using TRLevelControl; using TRLevelControl.Model; using TRRandomizerCore.Helpers; using TRRandomizerCore.Levels; @@ -10,20 +10,17 @@ namespace TRRandomizerCore.Randomizers; public class TR3StartPositionRandomizer : BaseTR3Randomizer { - private static readonly short _rotation = -8192; private Dictionary> _startLocations; public override void Randomize(int seed) { - _generator = new Random(seed); + _generator = new(seed); _startLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR3\Locations\start_positions.json")); foreach (TR3ScriptedLevel lvl in Levels) { LoadLevelInstance(lvl); - RandomizeStartPosition(_levelInstance); - SaveLevelInstance(); if (!TriggerProgress()) @@ -35,74 +32,50 @@ public override void Randomize(int seed) private void RandomizeStartPosition(TR3CombinedLevel level) { - TR3Entity lara = level.Data.Entities.Find(e => e.TypeID == (short)TR3Type.Lara); + TR3Entity lara = level.Data.Entities.Find(e => e.TypeID == TR3Type.Lara); // If we haven't defined anything for a level, Lara will just be rotated. This is most likely where there are // triggers just after Lara's starting spot, so we just skip them here. if (!Settings.RotateStartPositionOnly && _startLocations.ContainsKey(level.Name)) { List locations = _startLocations[level.Name]; - if (Settings.DevelopmentMode) + Location location; + do { - foreach (Location loc in locations) - { - level.Data.Entities.Add(new() - { - TypeID = (short)TR3Type.Lara, - X = loc.X, - Y = loc.Y, - Z = loc.Z, - Room = (short)loc.Room, - Angle = lara.Angle, - Intensity1 = -1, - Intensity2 = -1, - Flags = 0 - }); - } + location = locations[_generator.Next(0, locations.Count)]; } - else + while (!location.Validated); + + // If there are any triggers below Lara, move them + new EMMoveTriggerFunction { - Location location; - do + BaseLocation = new() { - location = locations[_generator.Next(0, locations.Count)]; - } - while (!location.Validated); - - // If there are any triggers below Lara, move them - new EMMoveTriggerFunction + X = lara.X, + Y = lara.Y, + Z = lara.Z, + Room = lara.Room + }, + NewLocation = new() { - BaseLocation = new EMLocation - { - X = lara.X, - Y = lara.Y, - Z = lara.Z, - Room = lara.Room - }, - NewLocation = new EMLocation - { - X = location.X, - Y = location.Y, - Z = location.Z, - Room = (short)location.Room - } - }.ApplyToLevel(level.Data); + X = location.X, + Y = location.Y, + Z = location.Z, + Room = (short)location.Room + } + }.ApplyToLevel(level.Data); - lara.X = location.X; - lara.Y = location.Y; - lara.Z = location.Z; - lara.Room = (short)location.Room; - lara.Angle = (short)(_generator.Next(0, 8) * _rotation); - } + lara.X = location.X; + lara.Y = location.Y; + lara.Z = location.Z; + lara.Room = (short)location.Room; } - else + + short currentAngle = lara.Angle; + do { - short currentAngle = lara.Angle; - do - { - lara.Angle = (short)(_generator.Next(0, 8) * _rotation); - } - while (lara.Angle == currentAngle); + lara.Angle = (short)(_generator.Next(0, 8) * -TRConsts.Angle45); } + while (lara.Angle == currentAngle); } } diff --git a/TRRandomizerCore/Randomizers/TR3R/BaseTR3RRandomizer.cs b/TRRandomizerCore/Randomizers/TR3R/BaseTR3RRandomizer.cs new file mode 100644 index 000000000..9149ddc94 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR3R/BaseTR3RRandomizer.cs @@ -0,0 +1,13 @@ +using TRRandomizerCore.Editors; +using TRRandomizerCore.Processors; + +namespace TRRandomizerCore.Randomizers; + +public abstract class BaseTR3RRandomizer : TR3RLevelProcessor, IRandomizer +{ + public RandomizerSettings Settings { get; internal set; } + + protected Random _generator; + + public abstract void Randomize(int seed); +} diff --git a/TRRandomizerCore/Randomizers/TR3R/TR3RAudioRandomizer.cs b/TRRandomizerCore/Randomizers/TR3R/TR3RAudioRandomizer.cs new file mode 100644 index 000000000..475fc2951 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR3R/TR3RAudioRandomizer.cs @@ -0,0 +1,252 @@ +using Newtonsoft.Json; +using System.Numerics; +using TRFDControl; +using TRFDControl.FDEntryTypes; +using TRFDControl.Utilities; +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRModelTransporter.Handlers; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; +using TRRandomizerCore.SFX; + +namespace TRRandomizerCore.Randomizers; + +public class TR3RAudioRandomizer : BaseTR3RRandomizer +{ + private const int _maxSample = 413; + private const int _defaultSecretTrack = 122; + + private AudioRandomizer _audioRandomizer; + + private List> _soundEffects; + private List _sfxCategories; + private List _uncontrolledLevels; + + public override void Randomize(int seed) + { + _generator = new(seed); + + LoadAudioData(); + ChooseUncontrolledLevels(); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + + RandomizeMusicTriggers(_levelInstance); + RandomizeSoundEffects(_levelInstance); + RandomizeWibble(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + 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) + { + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + if (Settings.ChangeTriggerTracks) + { + RandomizeFloorTracks(level.Data, floorData); + } + + if (Settings.SeparateSecretTracks) + { + RandomizeSecretTracks(level, floorData); + } + + floorData.WriteToLevel(level.Data); + } + + private void RandomizeFloorTracks(TR3Level level, FDControl floorData) + { + _audioRandomizer.ResetFloorMap(); + foreach (TR3Room room in level.Rooms) + { + _audioRandomizer.RandomizeFloorTracks(room.Sectors, 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, FDControl floorData) + { + 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; + } + + FDActionListItem musicAction = new() + { + TrigAction = FDTrigAction.PlaySoundtrack, + Parameter = secretTrack.ID + }; + + List triggers = FDUtilities.GetSecretTriggers(floorData, i); + foreach (FDTriggerEntry trigger in triggers) + { + FDActionListItem currentMusicAction = trigger.TrigActionList.Find(a => a.TrigAction == FDTrigAction.PlaySoundtrack); + if (currentMusicAction == null) + { + trigger.TrigActionList.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")); + } + } + + private void RandomizeSoundEffects(TR3RCombinedLevel level) + { + if (_sfxCategories.Count == 0) + { + return; + } + + if (IsUncontrolledLevel(level.Script)) + { + HashSet indices = new(); + while (indices.Count < level.Data.NumSampleIndices) + { + indices.Add((uint)_generator.Next(0, _maxSample + 1)); + } + level.Data.SampleIndices = indices.ToArray(); + } + else + { + for (int internalIndex = 0; internalIndex < level.Data.SoundMap.Length; internalIndex++) + { + TRSFXDefinition definition = _soundEffects.Find(sfx => sfx.InternalIndex == internalIndex); + if (level.Data.SoundMap[internalIndex] == -1 || 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) + { + TRSFXDefinition nextDefinition = otherDefinitions[_generator.Next(0, otherDefinitions.Count)]; + short soundDetailsIndex = ImportSoundEffect(level.Data, definition, nextDefinition); + if (soundDetailsIndex != -1) + { + level.Data.SoundMap[internalIndex] = soundDetailsIndex; + } + } + } + } + + SoundUtilities.ResortSoundIndices(level.Data); + } + + private static short ImportSoundEffect(TR3Level level, TRSFXDefinition currentDefinition, TRSFXDefinition newDefinition) + { + if (newDefinition.SampleIndices.Count == 0) + { + return -1; + } + + List levelSamples = level.SampleIndices.ToList(); + List levelSoundDetails = level.SoundDetails.ToList(); + + uint minSample = newDefinition.SampleIndices.Min(); + if (levelSamples.Contains(minSample)) + { + return (short)levelSoundDetails.FindIndex(d => levelSamples[d.Sample] == minSample); + } + + ushort newSampleIndex = (ushort)levelSamples.Count; + List sortedSamples = new(newDefinition.SampleIndices); + sortedSamples.Sort(); + levelSamples.AddRange(sortedSamples); + + level.SampleIndices = levelSamples.ToArray(); + level.NumSampleIndices = (uint)levelSamples.Count; + + levelSoundDetails.Add(new TR3SoundDetails + { + Chance = currentDefinition.Details.Chance, + Characteristics = newDefinition.Details.Characteristics, + Pitch = newDefinition.Details.Pitch, + Range = newDefinition.Details.Range, + Sample = newSampleIndex, + Volume = newDefinition.Details.Volume + }); + + level.SoundDetails = levelSoundDetails.ToArray(); + level.NumSoundDetails++; + + return (short)(level.NumSoundDetails - 1); + } + + private void RandomizeWibble(TR3RCombinedLevel level) + { + if (Settings.RandomizeWibble) + { + foreach (TR3SoundDetails details in level.Data.SoundDetails) + { + details.Wibble = true; + } + } + } +} diff --git a/TRRandomizerCore/Randomizers/TR3R/TR3REnvironmentRandomizer.cs b/TRRandomizerCore/Randomizers/TR3R/TR3REnvironmentRandomizer.cs new file mode 100644 index 000000000..404bc1565 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR3R/TR3REnvironmentRandomizer.cs @@ -0,0 +1,84 @@ +using TREnvironmentEditor; +using TREnvironmentEditor.Helpers; +using TREnvironmentEditor.Model; +using TRGE.Core; +using TRRandomizerCore.Levels; + +namespace TRRandomizerCore.Randomizers; + +public class TR3REnvironmentRandomizer : BaseTR3RRandomizer +{ + public override void Randomize(int seed) + { + _generator ??= new(seed); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + RandomizeEnvironment(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + public void FinalizeEnvironment() + { + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + FinalizeEnvironment(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + private EMEditorMapping GetMapping(TR3RCombinedLevel level) + => EMEditorMapping.Get(GetResourcePath($@"TR3\Environment\{level.Name}-Environment.json")); + + private void RandomizeEnvironment(TR3RCombinedLevel level) + { + EMEditorMapping mapping = GetMapping(level); + if (mapping != null) + { + ApplyMappingToLevel(level, mapping); + } + } + + private void ApplyMappingToLevel(TR3RCombinedLevel level, EMEditorMapping mapping) + { + EnvironmentPicker picker = new(Settings.HardEnvironmentMode) + { + Generator = _generator + }; + picker.LoadTags(Settings, true); + picker.Options.ExclusionMode = EMExclusionMode.Individual; + + mapping.All.ApplyToLevel(level.Data, picker.Options); + + // No further mods supported yet + } + + private void FinalizeEnvironment(TR3RCombinedLevel level) + { + EMEditorMapping mapping = GetMapping(level); + EnvironmentPicker picker = new(Settings.HardEnvironmentMode); + picker.Options.ExclusionMode = EMExclusionMode.Individual; + picker.ResetTags(true); + + if (mapping != null) + { + foreach (EMConditionalSingleEditorSet mod in mapping.ConditionalAll) + { + mod.ApplyToLevel(level.Data, picker.Options); + } + } + } +} diff --git a/TRRandomizerCore/Randomizers/TR3R/TR3RItemRandomizer.cs b/TRRandomizerCore/Randomizers/TR3R/TR3RItemRandomizer.cs new file mode 100644 index 000000000..3fec3eaf8 --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR3R/TR3RItemRandomizer.cs @@ -0,0 +1,283 @@ +using Newtonsoft.Json; +using TRFDControl; +using TRFDControl.Utilities; +using TRGE.Core; +using TRLevelControl.Helpers; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; +using TRRandomizerCore.Secrets; +using TRRandomizerCore.Utilities; + +namespace TRRandomizerCore.Randomizers; + +public class TR3RItemRandomizer : BaseTR3RRandomizer +{ + private readonly Dictionary> _excludedLocations, _pistolLocations; + private readonly LocationPicker _picker; + + private TRSecretMapping _secretMapping; + private TR3Entity _unarmedLevelPistols; + + public ItemFactory ItemFactory { get; set; } + + public TR3RItemRandomizer() + { + _excludedLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR3\Locations\invalid_item_locations.json")); + _pistolLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR3\Locations\unarmed_locations.json")); + _picker = new(GetResourcePath(@"TR3\Locations\routes.json")); + } + + public override void Randomize(int seed) + { + _generator = new(seed); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + FindUnarmedLevelPistols(_levelInstance); + + _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, false), Settings, _generator); + _secretMapping = TRSecretMapping.Get(GetResourcePath($@"TR3\SecretMapping\{_levelInstance.Name}-SecretMapping.json")); + + if (Settings.RandomizeItemTypes) + { + RandomizeItemTypes(_levelInstance); + } + + if (Settings.RandomizeItemPositions) + { + RandomizeItemLocations(_levelInstance); + } + + if (Settings.RandoItemDifficulty == ItemDifficulty.OneLimit) + { + EnforceOneLimit(_levelInstance); + } + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + public void RandomizeKeyItems() + { + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + RandomizeKeyItems(_levelInstance); + + SaveLevelInstance(); + if (!TriggerProgress()) + { + break; + } + } + } + + private void FindUnarmedLevelPistols(TR3RCombinedLevel level) + { + if (level.Script.RemovesWeapons) + { + _unarmedLevelPistols = level.Data.Entities.Find( + e => e.TypeID == TR3Type.Pistols_P + && _pistolLocations[level.Name].Any(l => l.IsEquivalent(e.GetLocation()))); + } + else + { + _unarmedLevelPistols = null; + } + } + + private List GetItemLocationPool(TR3RCombinedLevel level, bool keyItemMode) + { + List exclusions = new(); + if (_excludedLocations.ContainsKey(level.Name)) + { + exclusions.AddRange(_excludedLocations[level.Name]); + } + + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + foreach (TR3Entity entity in level.Data.Entities) + { + if (!TR3TypeUtilities.CanSharePickupSpace(entity.TypeID)) + { + exclusions.Add(entity.GetFloorLocation(loc => + FDUtilities.GetRoomSector(loc.X, loc.Y, loc.Z, (short)loc.Room, level.Data, floorData))); + } + } + + if (level.Script.HasColdWater) + { + // Don't put items underwater if it's too cold + for (int i = 0; i < level.Data.NumRooms; i++) + { + if (level.Data.Rooms[i].ContainsWater) + { + exclusions.Add(new() + { + Room = i, + InvalidatesRoom = true + }); + } + } + } + + TR3LocationGenerator generator = new(); + return generator.Generate(level.Data, exclusions, keyItemMode); + } + + public void RandomizeItemTypes(TR3RCombinedLevel level) + { + if (level.IsAssault) + { + return; + } + + List stdItemTypes = TR3TypeUtilities.GetStandardPickupTypes(); + stdItemTypes.Remove(TR3Type.PistolAmmo_P); + + bool hasPistols = level.Data.Entities.Any(e => e.TypeID == TR3Type.Pistols_P); + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR3Entity entity = level.Data.Entities[i]; + TR3Type entityType = entity.TypeID; + if (!TR3TypeUtilities.IsStandardPickupType(entityType) || _secretMapping.RewardEntities.Contains(i)) + { + continue; + } + + if (entity == _unarmedLevelPistols) + { + if (entityType == TR3Type.Pistols_P && Settings.GiveUnarmedItems) + { + do + { + entityType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; + } + while (!TR3TypeUtilities.IsWeaponPickup(entityType)); + entity.TypeID = entityType; + } + } + else if (TR3TypeUtilities.IsStandardPickupType(entityType)) + { + TR3Type newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; + if (newType == TR3Type.Pistols_P && (hasPistols || !level.Script.RemovesWeapons)) + { + do + { + newType = stdItemTypes[_generator.Next(0, stdItemTypes.Count)]; + } + while (!TR3TypeUtilities.IsWeaponPickup(newType) || newType == TR3Type.Pistols_P); + } + entity.TypeID = newType; + } + + hasPistols = level.Data.Entities.Any(e => e.TypeID == TR3Type.Pistols_P); + } + } + + public void RandomizeItemLocations(TR3RCombinedLevel level) + { + if (level.IsAssault) + { + return; + } + + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR3Entity entity = level.Data.Entities[i]; + if (!TR3TypeUtilities.IsStandardPickupType(entity.TypeID) + || _secretMapping.RewardEntities.Contains(i) + || ItemFactory.IsItemLocked(level.Name, i) + || entity == _unarmedLevelPistols) + { + continue; + } + + _picker.RandomizePickupLocation(entity); + } + } + + public void EnforceOneLimit(TR3RCombinedLevel level) + { + if (level.IsAssault) + { + return; + } + + HashSet uniqueTypes = new(); + if (_unarmedLevelPistols != null) + { + uniqueTypes.Add(_unarmedLevelPistols.TypeID); + } + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR3Entity entity = level.Data.Entities[i]; + if (_secretMapping.RewardEntities.Contains(i) || entity == _unarmedLevelPistols) + { + continue; + } + + if ((TR3TypeUtilities.IsStandardPickupType(entity.TypeID) || TR3TypeUtilities.IsCrystalPickup(entity.TypeID)) + && !uniqueTypes.Add(entity.TypeID)) + { + ItemUtilities.HideEntity(entity); + ItemFactory.FreeItem(level.Name, i); + } + } + } + + private void RandomizeKeyItems(TR3RCombinedLevel level) + { + FDControl floorData = new(); + floorData.ParseFromLevel(level.Data); + + _picker.TriggerTestAction = location => LocationUtilities.HasAnyTrigger(location, level.Data, floorData); + _picker.KeyItemTestAction = (location, hasPickupTrigger) => TestKeyItemLocation(location, hasPickupTrigger, level); + _picker.RoomInfos = level.Data.Rooms + .Select(r => new ExtRoomInfo(r.Info, r.NumXSectors, r.NumZSectors)) + .ToList(); + + _picker.Initialise(_levelInstance.Name, GetItemLocationPool(_levelInstance, true), Settings, _generator); + + for (int i = 0; i < level.Data.Entities.Count; i++) + { + TR3Entity entity = level.Data.Entities[i]; + if (!TR3TypeUtilities.IsKeyItemType(entity.TypeID) + || ItemFactory.IsItemLocked(level.Name, i)) + { + continue; + } + + _picker.RandomizeKeyItemLocation( + entity, LocationUtilities.HasPickupTriger(entity, i, level.Data, floorData), + level.Script.OriginalSequence, level.Data.Rooms[entity.Room].Info); + } + } + + private bool TestKeyItemLocation(Location location, bool hasPickupTrigger, TR3RCombinedLevel level) + { + // Make sure if we're placing on the same tile as an enemy, that the + // enemy can drop the item. + TR3Entity enemy = level.Data.Entities + .FindAll(e => TR3TypeUtilities.IsEnemyType(e.TypeID)) + .Find(e => e.GetLocation().IsEquivalent(location)); + + return enemy == null || (Settings.AllowEnemyKeyDrops && !hasPickupTrigger && TR3TypeUtilities.CanDropPickups + ( + TR3TypeUtilities.GetAliasForLevel(level.Name, enemy.TypeID), + !Settings.RandomizeEnemies || Settings.ProtectMonks + )); + } +} diff --git a/TRRandomizerCore/Randomizers/TR3R/TR3RStartPositionRandomizer.cs b/TRRandomizerCore/Randomizers/TR3R/TR3RStartPositionRandomizer.cs new file mode 100644 index 000000000..08a6de4bd --- /dev/null +++ b/TRRandomizerCore/Randomizers/TR3R/TR3RStartPositionRandomizer.cs @@ -0,0 +1,78 @@ +using Newtonsoft.Json; +using TREnvironmentEditor.Model.Types; +using TRGE.Core; +using TRLevelControl; +using TRLevelControl.Model; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; + +namespace TRRandomizerCore.Randomizers; + +public class TR3RStartPositionRandomizer : BaseTR3RRandomizer +{ + private Dictionary> _startLocations; + + public override void Randomize(int seed) + { + _generator = new(seed); + _startLocations = JsonConvert.DeserializeObject>>(ReadResource(@"TR3\Locations\start_positions.json")); + + foreach (TRRScriptedLevel lvl in Levels) + { + LoadLevelInstance(lvl); + RandomizeStartPosition(_levelInstance); + SaveLevelInstance(); + + if (!TriggerProgress()) + { + break; + } + } + } + + private void RandomizeStartPosition(TR3RCombinedLevel level) + { + TR3Entity lara = level.Data.Entities.Find(e => e.TypeID == TR3Type.Lara); + + if (!Settings.RotateStartPositionOnly && _startLocations.ContainsKey(level.Name)) + { + List locations = _startLocations[level.Name]; + Location location; + do + { + location = locations[_generator.Next(0, locations.Count)]; + } + while (!location.Validated); + + new EMMoveTriggerFunction + { + BaseLocation = new() + { + X = lara.X, + Y = lara.Y, + Z = lara.Z, + Room = lara.Room + }, + NewLocation = new() + { + X = location.X, + Y = location.Y, + Z = location.Z, + Room = (short)location.Room + } + }.ApplyToLevel(level.Data); + + lara.X = location.X; + lara.Y = location.Y; + lara.Z = location.Z; + lara.Room = (short)location.Room; + } + + short currentAngle = lara.Angle; + do + { + lara.Angle = (short)(_generator.Next(0, 8) * -TRConsts.Angle45); + } + while (lara.Angle == currentAngle); + } +} diff --git a/TRRandomizerCore/Resources/TR1/Audio/audio_tracks.json b/TRRandomizerCore/Resources/TR1/Audio/audio_tracks.json index 8cb20984b..2a7ab09ca 100644 --- a/TRRandomizerCore/Resources/TR1/Audio/audio_tracks.json +++ b/TRRandomizerCore/Resources/TR1/Audio/audio_tracks.json @@ -228,8 +228,7 @@ "Offset": 183093060, "Length": 9212874, "Categories": [ - 7, - 4 + 7 ], "PrimaryCategory": 7 }, @@ -240,7 +239,6 @@ "Length": 6919674, "Categories": [ 8, - 4, 7 ], "PrimaryCategory": 8 @@ -272,6 +270,7 @@ "Length": 2359146, "Categories": [ 5, + 4, 7 ], "PrimaryCategory": 5 diff --git a/TRRandomizerCore/Resources/TR2/Locations/unarmed_locations.json b/TRRandomizerCore/Resources/TR2/Locations/unarmed_locations.json index db876067c..574af7abe 100644 --- a/TRRandomizerCore/Resources/TR2/Locations/unarmed_locations.json +++ b/TRRandomizerCore/Resources/TR2/Locations/unarmed_locations.json @@ -2,8 +2,8 @@ "WALL.TR2": [ { "X": 47648, - "Z": 30256, "Y": -40, + "Z": 30256, "Room": 25 }, { @@ -2202,41 +2202,49 @@ "LEVEL1.TR2": [ { "X": 76274, - "Z": 72159, "Y": -3328, + "Z": 72159, "Room": 3 } ], "LEVEL2.TR2": [ { "X": 73318, - "Z": 63113, "Y": 4864, + "Z": 63113, "Room": 45 } ], "LEVEL3.TR2": [ { "X": 58860, - "Z": 34274, "Y": 0, + "Z": 34274, "Room": 68 } ], "LEVEL4.TR2": [ { "X": 37355, - "Z": 53844, "Y": 2410, + "Z": 53844, "Room": 2 } ], "LEVEL5.TR2": [ { "X": 64060, - "Z": 53754, "Y": 0, + "Z": 53754, "Room": 28 } + ], + "HOUSE.TR2": [ + { + "X": 30208, + "Y": -256, + "Z": 67072, + "Room": 57 + } ] } \ No newline at end of file diff --git a/TRRandomizerCore/TRRandomizerController.cs b/TRRandomizerCore/TRRandomizerController.cs index 09a7e3356..e6bb2b567 100644 --- a/TRRandomizerCore/TRRandomizerController.cs +++ b/TRRandomizerCore/TRRandomizerController.cs @@ -43,9 +43,9 @@ public bool IsRandomizationSupported(TRRandomizerType randomizerType) return TRVersionSupport.IsRandomizationSupported(_editor.Edition, randomizerType); } - public List GetExecutables() + public List GetExecutables(string dataFolder) { - return TRVersionSupport.GetExecutables(_editor.Edition); + return TRVersionSupport.GetExecutables(_editor.Edition, dataFolder); } public bool IsTR1 => _editor.Edition.Version == TRVersion.TR1; @@ -89,7 +89,7 @@ private void SetScriptSequencing() { if (RandomizeGameMode) { - ScriptEditor.LevelSequencingOrganisation = RandomizeSequencing ? Organisation.Random : Organisation.Default; + ScriptEditor.LevelSequencingOrganisation = (RandomizeSequencing || _editor.Edition.Remastered) ? Organisation.Random : Organisation.Default; ScriptEditor.EnabledLevelOrganisation = Organisation.Random; ScriptEditor.GameMode = LevelRandomizer.GameMode; } diff --git a/TRRandomizerCore/TRRandomizerCoord.cs b/TRRandomizerCore/TRRandomizerCoord.cs index 2770bd3f4..813a7b563 100644 --- a/TRRandomizerCore/TRRandomizerCoord.cs +++ b/TRRandomizerCore/TRRandomizerCoord.cs @@ -53,13 +53,12 @@ public void Initialise(string applicationID, string version, string taggedVersio TRInterop.SecretRewardsSupported = true; TRInterop.ChecksumTester = new ChecksumTester(); - TRLevelEditorFactory.RegisterEditor(TRVersion.TR1, typeof(TR1RandoEditor)); - TRLevelEditorFactory.RegisterEditor(TRVersion.TR2, typeof(TR2RandoEditor)); - TRLevelEditorFactory.RegisterEditor(TRVersion.TR3, typeof(TR3RandoEditor)); - - // Not yet fully supported i.e. no locations, textures etc defined - //TRLevelEditorFactory.RegisterEditor(TRVersion.TR2G, typeof(TR2RandoEditor)); - //TRLevelEditorFactory.RegisterEditor(TRVersion.TR3G, typeof(TR3RandoEditor)); + TRLevelEditorFactory.RegisterEditor(TREdition.TR1PC, typeof(TR1ClassicEditor)); + TRLevelEditorFactory.RegisterEditor(TREdition.TR2PC, typeof(TR2ClassicEditor)); + TRLevelEditorFactory.RegisterEditor(TREdition.TR3PC, typeof(TR3ClassicEditor)); + TRLevelEditorFactory.RegisterEditor(TREdition.TR1RM, typeof(TR1RemasteredEditor)); + TRLevelEditorFactory.RegisterEditor(TREdition.TR2RM, typeof(TR2RemasteredEditor)); + TRLevelEditorFactory.RegisterEditor(TREdition.TR3RM, typeof(TR3RemasteredEditor)); // #125 Invoke TRCoord.Instance after defining TRInterop.ExecutingVersionName otherwise // TRGE will not know the config file name to look for. @@ -71,8 +70,8 @@ public void Initialise(string applicationID, string version, string taggedVersio public TRRandomizerController Open(string directoryPath, bool performChecksumTest) { - _openEventArgs = new TROpenRestoreEventArgs(); - return new TRRandomizerController(directoryPath, performChecksumTest); + _openEventArgs = new(); + return new(directoryPath, performChecksumTest); } public static void ClearHistory() diff --git a/TRRandomizerCore/TRRandomizerType.cs b/TRRandomizerCore/TRRandomizerType.cs index 66d674e36..9c72edd4f 100644 --- a/TRRandomizerCore/TRRandomizerType.cs +++ b/TRRandomizerCore/TRRandomizerType.cs @@ -63,4 +63,6 @@ public enum TRRandomizerType ShortcutFixes, KeyContinuity, GameMode, + ItemDrops, + LevelCount, } diff --git a/TRRandomizerCore/TRVersionSupport.cs b/TRRandomizerCore/TRVersionSupport.cs index 1ef434a1d..784bce592 100644 --- a/TRRandomizerCore/TRVersionSupport.cs +++ b/TRRandomizerCore/TRVersionSupport.cs @@ -28,10 +28,12 @@ internal class TRVersionSupport TRRandomizerType.Health, TRRandomizerType.HiddenEnemies, TRRandomizerType.Item, + TRRandomizerType.ItemDrops, TRRandomizerType.ItemSprite, TRRandomizerType.KeyItems, TRRandomizerType.KeyItemTextures, TRRandomizerType.LarsonBehaviour, + TRRandomizerType.LevelCount, TRRandomizerType.LevelSequence, TRRandomizerType.Mediless, TRRandomizerType.MeshSwaps, @@ -54,6 +56,15 @@ internal class TRVersionSupport TRRandomizerType.WaterColour, }; + private static readonly List _tr1RTypes = new() + { + TRRandomizerType.Audio, + TRRandomizerType.Item, + TRRandomizerType.KeyItems, + TRRandomizerType.SecretAudio, + TRRandomizerType.StartPosition, + }; + private static readonly List _tr2Types = new() { TRRandomizerType.AmbientTracks, @@ -71,10 +82,12 @@ internal class TRVersionSupport TRRandomizerType.GlitchedSecrets, TRRandomizerType.HardSecrets, TRRandomizerType.Item, + TRRandomizerType.ItemDrops, TRRandomizerType.KeyContinuity, TRRandomizerType.KeyItems, TRRandomizerType.KeyItemTextures, TRRandomizerType.Ladders, + TRRandomizerType.LevelCount, TRRandomizerType.LevelSequence, TRRandomizerType.MeshSwaps, TRRandomizerType.NightMode, @@ -95,6 +108,17 @@ internal class TRVersionSupport TRRandomizerType.ItemSprite }; + private static readonly List _tr2RTypes = new() + { + TRRandomizerType.Audio, + TRRandomizerType.Item, + TRRandomizerType.ItemDrops, + TRRandomizerType.KeyItems, + TRRandomizerType.SecretAudio, + TRRandomizerType.SFX, + TRRandomizerType.StartPosition, + }; + private static readonly List _tr3Types = new() { TRRandomizerType.AmbientTracks, @@ -109,8 +133,10 @@ internal class TRVersionSupport TRRandomizerType.GlobeDisplay, TRRandomizerType.HardSecrets, TRRandomizerType.Item, + TRRandomizerType.ItemDrops, TRRandomizerType.KeyItems, TRRandomizerType.Ladders, + TRRandomizerType.LevelCount, TRRandomizerType.LevelSequence, TRRandomizerType.NightMode, TRRandomizerType.Outfit, @@ -134,23 +160,38 @@ internal class TRVersionSupport TRRandomizerType.Weather }; + private static readonly List _tr3RTypes = new() + { + TRRandomizerType.Audio, + TRRandomizerType.Item, + TRRandomizerType.ItemDrops, + TRRandomizerType.KeyItems, + TRRandomizerType.SecretAudio, + TRRandomizerType.SFX, + TRRandomizerType.StartPosition, + }; + private static readonly Dictionary _supportedTypes = new() { [TRVersion.TR1] = new() { - DefaultSupport = _tr1Types + DefaultSupport = _tr1Types, + RemasterSupport = _tr1RTypes, }, [TRVersion.TR2] = new() { - DefaultSupport = _tr2Types + DefaultSupport = _tr2Types, + RemasterSupport = _tr2RTypes, }, [TRVersion.TR3] = new() { DefaultSupport = _tr3Types, - PatchSupport = _tr3MainTypes + PatchSupport = _tr3MainTypes, + RemasterSupport = _tr3RTypes, } }; + private static readonly string _trrExe = "tomb123.exe"; private static readonly Dictionary> _versionExes = new() { [TRVersion.TR1] = new() { "TR1X.exe" }, @@ -180,15 +221,27 @@ public static bool IsRandomizationSupported(TREdition edition, TRRandomizerType supported = supportGroup.PatchSupport.Contains(randomizerType); } + // More limited in the Remasters + if (edition.Remastered) + { + supported = supportGroup.HasRemasterSupport + && supportGroup.RemasterSupport.Contains(randomizerType); + } + return supported; } - public static List GetExecutables(TREdition edition) + public static List GetExecutables(TREdition edition, string dataFolder) { List exes = new(); - if (_versionExes.ContainsKey(edition.Version)) + if (edition.Remastered) + { + exes.Add(Path.GetFullPath(Path.Combine(dataFolder, "../../", _trrExe))); + } + else if (_versionExes.ContainsKey(edition.Version)) { - exes.AddRange(_versionExes[edition.Version]); + exes.AddRange(_versionExes[edition.Version].Select( + p => Path.GetFullPath(Path.Combine(dataFolder, "../", p)))); } return exes; } @@ -198,5 +251,7 @@ internal class TRVersionSupportGroup { internal List DefaultSupport { get; set; } internal List PatchSupport { get; set; } + internal List RemasterSupport { get; set; } internal bool HasPatchSupport => PatchSupport != null; + internal bool HasRemasterSupport => RemasterSupport != null; } diff --git a/TRRandomizerView/Controls/EditorControl.xaml b/TRRandomizerView/Controls/EditorControl.xaml index 59698e3be..f3e7b3504 100644 --- a/TRRandomizerView/Controls/EditorControl.xaml +++ b/TRRandomizerView/Controls/EditorControl.xaml @@ -38,6 +38,7 @@ + + - - - - - - - - - - @@ -343,22 +327,10 @@ - - @@ -366,36 +338,10 @@ - - - - - - - - - - @@ -408,6 +354,7 @@ BoolItemsSource="{Binding Data.AudioBoolItemControls, Source={StaticResource proxy}}" HasBoolItems="True" HasAudioOptions="True" + HasSFXOptions="{Binding Data.IsSFXTypeSupported, Source={StaticResource proxy}}" ControllerProxy="{Binding Data, Source={StaticResource proxy}}"> @@ -599,14 +546,14 @@ Command="cmds:WindowCommands.OpenGlobalSettingsCommand"/>