Skip to content

Commit

Permalink
Add TRR sequencing support (#741)
Browse files Browse the repository at this point in the history
Resolves #756.
  • Loading branch information
lahm86 authored Aug 18, 2024
1 parent 604b768 commit 3924113
Show file tree
Hide file tree
Showing 19 changed files with 471 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## [Unreleased](https://github.com/LostArtefacts/TR-Rando/compare/V1.9.1...master) - xxxx-xx-xx
- added support for level sequence randomization in TR1R and TR2R (#756)
- added options to use textures from specific game areas only in TRR texture randomization (#726)
- changed vehicle randomization in TR2 so that it is now optional within item randomization (#750)
- fixed key item softlocks in remastered New Game+ when using shuffled item mode (#732, #734)
Expand Down
Binary file modified Deps/TRGE.Coord.dll
Binary file not shown.
Binary file modified Deps/TRGE.Core.dll
Binary file not shown.
30 changes: 30 additions & 0 deletions Resources/Documentation/TRR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# TR I-III Remastered Options

Note that for TR I-III Remastered, the options that are available to randomize are much more limited than the classics. The following table shows what can be randomized; the remaining options will be greyed out in the UI.

| Aspect | TR1R | TR2R | TR3R | Notes |
|-|-|-|-|-|
| Level sequencing | Yes | Yes | No | Some levels cannot be moved because of hard-coded logic in-game. For TR3, there are too many issues to work around to make this feature feasible. |
| Unarmed/ammoless levels | No | No | No | This is hard-coded in the games and cannot be controlled. |
| Night mode/VFX/sunsets | No | No | No | Currently, remastered rooms cannot be manipulated. |
| Secrets | Yes | Yes | Yes | - |
| Items | Yes | Yes | Yes | - |
| Enemies | Yes | Yes | Yes | - |
| Textures | Yes | Yes | Yes | This currently differs from classic in that textures are moved around between levels rather than hues/colours being changed or swapped. |
| Secret rewards | Yes | No | Yes | TR1 and TR3 will default to stacked rewards because reward rooms are not possible currently. TR2 rewards are hard-coded. |
| Audio | Yes | Yes | Yes | TR1 SFX randomization is slightly more limited than in classic (mainly weapon sounds). |
| Text | Yes | Yes | Yes | Only English is currently supported. |
| Environment | No | No | No | This requires further understanding of the remastered room structures, so is disabled entirely for the time being. |

## Level Sequencing

In the classics, the gameflow is entirely configurable, which means we can control everything related to level sequencing changes. The gameflow is hard-coded in TRR, so we have to work around this by renaming data files. This can produce some side effects when levels are off-sequence, which are pointed out below. These are issues that we cannot control and/or fix.

- Key item names will generally default to "Key Item"
- Lara will always lose her guns/ammo on level slots where this originally happens (i.e. Natla's Mines, Offshore Rig and Home Sweet Home)
- Cutscenes and FMVs are hard-coded, so will always follow the original level sequencing e.g. Larson vs. Lara will always show after the fourth level in TR1R
- You may notice some minor visual glitches in remastered graphics, such as the skybox flickering occasionally in Colosseum
- Ember colours in TR2 will change depending on the sequence e.g. in the Diving Area slot they are green, otherwise they are orange
- In Barkhang Monastery, the Seraph will not be in Lara's inventory. A workaround is provided at the end of this level, such that the Seraph is not required

Another point to stress is that if you need to look up items in trview, you will need to open the level file whose name matches the sequence you are currently on. For example, if you are playing Great Wall but the sequence is what would normally be Living Quarters, open `LIVING.TR2` rather than `WALL.TR2`.
5 changes: 5 additions & 0 deletions TRLevelControl/Helpers/TR2TypeUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,11 @@ public static bool IsStandardPickupType(TR2Type type)
return GetStandardPickupTypes().Contains(type);
}

public static bool IsMediType(TR2Type type)
{
return type == TR2Type.SmallMed_S_P || type == TR2Type.LargeMed_S_P;
}

public static List<TR2Type> GetKeyItemTypes()
{
return new()
Expand Down
28 changes: 25 additions & 3 deletions TRLevelControl/Model/Common/FloorData/FDControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ public IEnumerator<KeyValuePair<int, List<FDEntry>>> GetEnumerator()
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();

public IEnumerable<FDEntry> FindAll(Func<FDEntry, bool> predicate)
public IEnumerable<T> FindAll<T>(Func<T, bool> predicate)
where T : FDEntry
{
return _entries.Values.SelectMany(v => v.Where(predicate));
return _entries.Values.SelectMany(v => v)
.OfType<T>()
.Where(predicate);
}

public FDControl(TRGameVersion version, ITRLevelObserver observer, ushort dummyData = 0)
Expand Down Expand Up @@ -129,7 +132,26 @@ public List<FDTriggerEntry> GetTriggers(FDTrigAction action, int parameter = -1)
.ToList();
}

public List<FDTriggerEntry> GetSwitchKeyTriggers(int entityIndex)
{
return FindAll<FDTriggerEntry>(t =>
(t.TrigType == FDTrigType.Switch || t.TrigType == FDTrigType.Key)
&& t.SwitchOrKeyRef == entityIndex)
.ToList();
}

public List<FDActionItem> GetActionItems(FDTrigAction action, int sectorIndex = -1)
{
return GetActionItems(new List<FDTrigAction> { action }, sectorIndex);
}

public List<FDActionItem> GetEntityActionItems(int entityIndex)
{
return GetActionItems(new List<FDTrigAction> { FDTrigAction.Object, FDTrigAction.LookAtItem })
.FindAll(a => a.Parameter == entityIndex);
}

public List<FDActionItem> GetActionItems(List<FDTrigAction> actions, int sectorIndex = -1)
{
List<List<FDEntry>> entrySearch;
if (sectorIndex == -1)
Expand All @@ -147,7 +169,7 @@ public List<FDActionItem> GetActionItems(FDTrigAction action, int sectorIndex =
return entrySearch
.SelectMany(e => e.Where(i => i is FDTriggerEntry))
.Cast<FDTriggerEntry>()
.SelectMany(t => t.Actions.FindAll(a => a.Action == action))
.SelectMany(t => t.Actions.FindAll(a => actions.Contains(a.Action)))
.ToList();
}

Expand Down
22 changes: 22 additions & 0 deletions TRLevelControlTests/TR1/FDTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,28 @@ public void GetTriggerActions()
Assert.AreEqual(1, musicActions.Count);
}

[TestMethod]
public void GetSwitchKeyTriggers()
{
TR1Level level = GetTR1TestLevel();

int itemIndex = level.Entities.FindIndex(e => e.TypeID == TR1Type.WallSwitch);
Assert.AreNotEqual(-1, itemIndex);

List<FDTriggerEntry> triggers = level.FloorData.GetSwitchKeyTriggers(itemIndex);
Assert.AreEqual(1, triggers.Count);
Assert.AreEqual(FDTrigType.Switch, triggers[0].TrigType);
Assert.AreEqual(itemIndex, triggers[0].SwitchOrKeyRef);

itemIndex = level.Entities.FindIndex(e => e.TypeID == TR1Type.Keyhole1);
Assert.AreNotEqual(-1, itemIndex);

triggers = level.FloorData.GetSwitchKeyTriggers(itemIndex);
Assert.AreEqual(1, triggers.Count);
Assert.AreEqual(FDTrigType.Key, triggers[0].TrigType);
Assert.AreEqual(itemIndex, triggers[0].SwitchOrKeyRef);
}

[TestMethod]
[Description("Test removing entity triggers.")]
public void RemoveEntityTriggers()
Expand Down
21 changes: 21 additions & 0 deletions TRRandomizerCore/Editors/TR1RemasteredEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using TRGE.Core;
using TRLevelControl.Model;
using TRRandomizerCore.Helpers;
using TRRandomizerCore.Processors;
using TRRandomizerCore.Randomizers;
using TRRandomizerCore.Secrets;

Expand Down Expand Up @@ -69,6 +70,12 @@ protected override int GetSaveTarget(int numLevels)
// Environment randomizer always runs
target += numLevels * 2;

// Level sequencing checks
if (Settings.RandomizeGameMode)
{
target += numLevels;
}

return target;
}

Expand Down Expand Up @@ -242,6 +249,20 @@ protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMoni
}.Randomize(Settings.TextureSeed);
}

if (!monitor.IsCancelled && Settings.RandomizeGameMode)
{
monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Running level sequence checks");
new TR1RSequenceProcessor
{
ScriptEditor = scriptEditor,
Levels = levels,
BasePath = wipDirectory,
BackupPath = backupDirectory,
SaveMonitor = monitor,
Settings = Settings,
}.Run();
}

monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Finalizing tasks - please wait");
titleTask.Wait();
}
Expand Down
21 changes: 21 additions & 0 deletions TRRandomizerCore/Editors/TR2RemasteredEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using TRGE.Core;
using TRLevelControl.Model;
using TRRandomizerCore.Helpers;
using TRRandomizerCore.Processors;
using TRRandomizerCore.Randomizers;

namespace TRRandomizerCore.Editors;
Expand Down Expand Up @@ -63,6 +64,12 @@ protected override int GetSaveTarget(int numLevels)
// Environment randomizer always runs
target += numLevels * 2;

// Level sequencing checks
if (Settings.RandomizeGameMode)
{
target += numLevels;
}

return target;
}

Expand Down Expand Up @@ -223,6 +230,20 @@ protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMoni
}.Randomize(Settings.TextureSeed);
}

if (!monitor.IsCancelled && Settings.RandomizeGameMode)
{
monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Running level sequence checks");
new TR2RSequenceProcessor
{
ScriptEditor = scriptEditor,
Levels = levels,
BasePath = wipDirectory,
BackupPath = backupDirectory,
SaveMonitor = monitor,
Settings = Settings,
}.Run();
}

monitor.FireSaveStateBeginning(TRSaveCategory.Custom, "Finalizing tasks - please wait");
titleTask.Wait();
}
Expand Down
14 changes: 14 additions & 0 deletions TRRandomizerCore/Processors/AbstractLevelProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ public AbstractLevelProcessor()
_maxThreads = 3;
}

protected void Process(Action<C> processAction)
{
foreach (S lvl in Levels)
{
LoadLevelInstance(lvl);
processAction(_levelInstance);
SaveLevelInstance();
if (!TriggerProgress())
{
break;
}
}
}

protected void LoadLevelInstance(S scriptedLevel)
{
_levelInstance = LoadCombinedLevel(scriptedLevel);
Expand Down
75 changes: 75 additions & 0 deletions TRRandomizerCore/Processors/BaseTRRSequenceProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System.Diagnostics;
using TRGE.Core;
using TRLevelControl.Model;

namespace TRRandomizerCore.Processors;

public class BaseTRRSequenceProcessor<E, T>
where E : TREntity<T>
where T : Enum
{
public Func<T, bool> IsMediType { get; set; }

public void AdjustStrings(TRRScript script)
{
// 90% of key items will default to "Select Level", which itself it not used
// anywhere else in the game. This makes it look a little better.
script.CommonStrings["SELECTLEV"] = "Key Item";
}

public void AdjustMedipacks(TRRScriptedLevel levelScript, List<E> currentItems, List<E> originalItems,
E dummyItem, FDControl floorData, List<int> freeIndices = null)
{
// In NG+, the game will convert medipacks to ammo, but this is based on the items' indices
// in the level's original slot. So we need to guarantee that the items match up in the new
// slot to avoid the wrong things being converted.
if (levelScript.Sequence == levelScript.OriginalSequence)
{
return;
}

List<int> ogIndices = GetMediIndices(originalItems);
Queue<int> swappableIndices = new(GetMediIndices(currentItems).Except(ogIndices));
freeIndices?.ForEach(i => swappableIndices.Enqueue(i));

foreach (int index in ogIndices)
{
while (currentItems.Count <= index)
{
currentItems.Add(dummyItem);
}

E entity = currentItems[index];
if (IsMediType(entity.TypeID))
{
continue;
}

if (swappableIndices.Count == 0)
{
swappableIndices.Enqueue(currentItems.Count);
currentItems.Add(dummyItem);
}
int swapIndex = swappableIndices.Dequeue();
currentItems[index] = currentItems[swapIndex];
currentItems[swapIndex] = entity;

floorData.GetEntityActionItems(index)
.ForEach(a => a.Parameter = (short)swapIndex);

floorData.GetSwitchKeyTriggers(index)
.ForEach(t => t.SwitchOrKeyRef = (short)swapIndex);
}

// Sanity check
ogIndices.ForEach(i => Debug.Assert(currentItems[i] == dummyItem
|| IsMediType(currentItems[i].TypeID)));
}

private List<int> GetMediIndices(List<E> items)
{
return items.Where(e => IsMediType(e.TypeID))
.Select(e => items.IndexOf(e))
.ToList();
}
}
42 changes: 42 additions & 0 deletions TRRandomizerCore/Processors/TR1/TR1RSequenceProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using TRGE.Core;
using TRLevelControl.Helpers;
using TRLevelControl.Model;
using TRRandomizerCore.Editors;
using TRRandomizerCore.Levels;
using TRRandomizerCore.Utilities;

namespace TRRandomizerCore.Processors;

public class TR1RSequenceProcessor : TR1RLevelProcessor
{
private static readonly TR1Entity _dummyItem = new()
{
TypeID = TR1Type.CameraTarget_N,
Invisible = true,
};

private BaseTRRSequenceProcessor<TR1Entity, TR1Type> _processor;

public RandomizerSettings Settings { get; set; }

public void Run()
{
_processor = new()
{
IsMediType = t => TR1TypeUtilities.IsMediType(t),
};
_processor.AdjustStrings(ScriptEditor.Script as TRRScript);
Process(AdjustLevel);
}

private void AdjustLevel(TR1RCombinedLevel level)
{
TRRScriptedLevel mimickedLevelScript = Levels.Find(l => l.OriginalSequence == level.Script.Sequence);
TR1Level mimickedLevel = LoadLevelData(Path.Combine(BackupPath, mimickedLevelScript.LevelFileBaseName));

TR1Entity dummyItem = (TR1Entity)_dummyItem.Clone();
dummyItem.SetLocation(level.Data.Entities.Find(e => e.TypeID == TR1Type.Lara).GetLocation());

_processor.AdjustMedipacks(level.Script, level.Data.Entities, mimickedLevel.Entities, dummyItem, level.Data.FloorData);
}
}
Loading

0 comments on commit 3924113

Please sign in to comment.