Skip to content

V1.3.0

Compare
Choose a tag to compare
@DanzaG DanzaG released this 29 May 16:26
· 1434 commits to master since this release

Main issues covered: #43, #58, #97, #124, #125

The changes are separated into the following sections.

  1. Dependency Changes
  2. TRGE Updates
  3. Cross-level Enemies
  4. Texture Framework
  5. Multiple Unarmed Locations
  6. Item Randomizer
  7. Refactoring
  8. UI Updates
  9. Misc
  10. Key Items

Dependency Changes

TRGE Updates

  • The "Modified by…" string that appears in the title screen/inventory can now be specified in German and French as well as in English. The language is detected in the script file.
  • Fixes a problem where disabling title screen demos was ineffective.
  • Fixes a problem with Floating Islands if the version swapper tool had been used after TRGE had performed a backup of the original files. The swapper tool replaces FLOATING.TR2, but TRGE will always use the file that it originally backed up. Checks will now detect when the swapper has been used, and the backed-up file will be replaced. For the Randomizer, this meant that despite switching to UK Box, it was possible to still see texture corruption in this level.
  • Fixes sprite texture bounds of imported weapons (Floating Islands and Dragon's Lair).
  • Fixes air bubbles showing as blood splats when swimming in HSH.
  • Fixes upside-down paintings in HSH piano room (original bug in the game).

#97 Cross-level Enemies

Excluding a few restrictions, any enemy type can now appear in any level. TRModelTransporter is responsible for exporting enemy definitions (or more loosely, model definitions) to JSON format, and these definitions are made up of animations, animation frames, cinematic frames, colours, sound, meshes, meshtrees, object textures, sprite textures and sprite sequences. Beside each JSON file is a PNG of the textures used by the model. The definition holds a map of how to relate the PNG to the object or sprite textures used by the model.

Some models have several dependencies on others. A good example is MarcoBartoli – this definition holds a reference to the dragon explosion (4 separate models), the dragon itself (another 4 separate models) and the model that defines Lara's animation of inspecting the dagger. The Venice goons and the Tibet mercenaries are others like this, all sharing various behaviour. This complexity is all handled within TRModelImporter, so given an entity to import, it will resolve all dependencies.

The models are exported once and stored in Resources\Models, rather than exporting each time they are needed.

Aliases

Some enemies have aliases, so these are models with the same ID, animations, sounds etc, but different textures. These have been defined as follows to allow specific entity types to be imported (the original levels they appear in are shown in brackets).

  • Barracuda
    • BarracudaIce (CoT, Ice Palace)
    • BarracudaUnwater (40F, Doria, LQ, Deck)
    • BarracudaXian (Xian)
  • StickWieldingGoon1
    • StickWieldingGoon1Bandana (Rig, Diving Area)
    • StickWieldingGoon1BlackJacket (HSH)
    • StickWieldingGoon1BodyWarmer (Venice)
    • StickWieldingGoon1GreenVest (40F, Doria, LQ, Deck)
    • StickWieldingGoon1WhiteVest (Bartoli, Opera)
  • TigerOrSnowLeopard
    • BengalTiger (Great Wall, Xian)
    • SnowLeopard (Tibet, CoT)
    • WhiteTiger (Ice Palace)

Only one entity per family can exist in a level as the model can only point to one set of textures.

Limits

The biggest limit is that only 16 texture tiles are supported per level. When transporting models, the textures of those no longer in use are removed from the tiles, so freeing up some space. Fortunately, Core duplicated a lot of textures in the original levels so there has been room for manipulation here to make even more space available. This was done by going through every texture and comparing it pixel-by-pixel with every other texture. On an exact match, the first texture is removed from the tile, and the texture object updated to point to the second tile location instead. For example, in tile index 3 in Great Wall, the 64x64 square at position [64, 152] is already contained within the 64x80 rectangle at position [72, 0], so the square is deleted and the texture object is repointed to [72, 0] - so gaining 64x64 pixels of space. JSON files are stored in Resources\Textures\Deduplication for each level, and these are used to process the remapping during randomization (this is much more efficient than running pixel-by-pixel checks each time). The mapping files also detail any dependencies, so for example if two enemies share the same texture, the area in the tile will only be removed if both enemies are also being removed.

Another limit is that the game only supports 2048 texture objects (TRLevelReader.Model.TR2Level.NumObjectTextures). When importing enemies, we re-use all of the texture objects of the removed enemies, but in some cases this is still not enough. An example is in Diving Area, which only has 26 free slots, so if the enemies that are being imported are more texture-heavy than the ones removed, we hit a problem. A workaround is described below in the randomization process.

Packing

After the tiles have been stripped of duplicates and the old enemies have been removed, the new enemy textures can be imported into the tiles. RectanglePacker deals with this bin-packing problem – you can see a demo of this in action at https://github.com/lahm86/RectanglePacker/raw/main/Resources/PackingDemo.gif. It would be ideal to start completely afresh and repack every texture, similar to the demo, but this takes between 10-20 seconds per level. For efficiency, we begin with the pre-populated level tiles and start packing on the last occupied tile, then fill them up until we reach the end of tile 16. If there are still more textures to add, we move to the first tile and continue from there. RectanglePacker is only one solution to bin-packing, so there is definitely room for improvement with it.

Randomization Process

Selecting Enemies

EnemyRandomizer has been updated with the option to include cross-level enemies. If this is not used, the standard native enemy randomization will take place. The number of enemy types per level remains the same for the bulk of levels, but for some there will be more and others less. The likes of Great Wall has plenty of tile space and free object textures, so we add an extra 2 enemy types here to make 6. Opera House and Diving Area are tight on space, so we drop 1 each here to 6 and 5.

TR2RandomizerCore.Utilities.EnemyUtilities contains most of the rules that control which enemies can be selected. Other than the special cases below, enemies are simply chosen at random, but we still honour the previous requirements for water and droppable enemies as well as ensuring that the chicken, dragon, and HSH shotgun goon remain at the end of their default levels. A check is also done at the end of Diving Area to ensure that both enemies here can be killed (so we specifically avoid the eels).

HSH

The kill count seems to break in HSH when you change the enemy types – in some cases you can finish the level after one kill, in others, the ShotgunGoon never spawns. So for now, we only randomize StickWieldingGoon1 into a different alias for this level.

Dragon

The dragon can only appear in the following levels outside of Lair.

  • Great Wall
  • Opera House
  • Doria
  • The Deck
  • Tibet
  • CoT

The main reason is that the explosion has a 128x128 texture, so levels tight on space won't support this without performing full texture repacking. In addition, if the dragon spawns in a compact room, it can be impossible to kill. It tends to land in the void when it falls so the dagger can't be reached. Lara can also void during the explosion. So, those levels that can support the textures have rooms defined for supporting the dragon. These are held in Resources\enemy_restrictions.json, which is a map of level name -> entity ID -> room number list.

In addition to room restrictions, the dragon will only appear a maximum of once per level. If multiple dragons spawn close together, the game tends to crash.

MercSnowmobDriver

The file described above also lists room restrictions for MercSnowmobDriver. This is restricted to showing a maximum of twice per level (outside of Tibet). As the black skidoo depends on the red skidoo, this will be added as an entity to a level when MercSnowmobDriver is present. One red skidoo is added to the same spot as the first MercSnowmobDriver. This should ideally be zoned along with the room restrictions for this enemy and the dragon.

Chicken

The chicken is restricted to appear in at most 3 different levels (there are currently no restrictions on location or count within each level). There is also scope in EnemyUtilities to limit other enemies across the game in a similar way if needed.

Performance

Texture packing is quite time consuming – on average it takes around 5 seconds per level. As a result, EnemyRandomizer uses threading to run the model import work in parallel.

Packing can fail if an enemy combination won't fit into a level (lack of tile space or free object textures). We could in theory pre-test all combinations but the time required to do this is too great. For Maria Doria alone there are around 25,000,000 possible enemy combinations, so at roughly 5 seconds per test, that would take almost 4 years...

As a workaround, 5 potential enemy combinations are allocated per level and an import attempt is then made on each combination until one succeeds. The enemy selection is first carried out synchronously for all levels so to ensure consistent allocation. The import/packing attempts are then done in parallel. Once the processing is complete, the actual changing of the level entities is done synchronously, again for consistent allocation. If all 5 attempts fail, native enemy randomization will take place instead for the affected level.

Old Enemies

Although the textures of old enemies will have been removed, the models, meshes, frames etc will all still be present in the levels. This is to avoid having to reindex arrays and everything that points to the arrays, and as far as I know there are no limits on these as seen in ObjectTextures. The old models will still work if they are added to the level as entities, the textures just won't be what you would expect. This isn't currently done, but it might be an interesting option to pursue.

Known Issues

  • When Lara retrieves the dagger from the dragon, the cinematic cutscene you would normally see in Lair doesn't seem to work in other levels. TRModelTransporter does import the cinematic frames for this, as without them the game crashes altogether when you try to get the dagger. With them in place, there is no crash but Lara ends up in a state with the hips model placeholder around her wrist. Drawing guns fixes this, and the dragon does end up turning to bones, so gameplay isn't badly affected.
  • Entity ID 12 contains some Lara death animations specific to certain enemies e.g. death-by-Yeti, death-by-Xian-Guard etc. Unfortunately I think the animation index that is selected is hard-coded, so if a level already has entity ID 12 defined, the new animations will not be imported. This just means that in those levels, if Lara is killed by any of these enemies, the "normal" death animation seems to be used.

#58 Texture Framework

Multiple texture groups can now be changed per level. This is achieved by splitting the components into their own parts and cherry-picking what we want to include in each level. The previous tile PNGs in Resources\TexturePacks are no longer needed: the Resources\Textures folder takes its place, and its contents are described below. There should be a considerable reduction in the release zip file size.

Deduplication

The sub-folder Resources\Textures\Deduplication is described above in enemy limitations. Deduplication will take place if cross-level enemies are chosen OR if texture randomization is chosen. This is because the texture framework is optimised to target the tiles that have been deduplicated.

Static Sources and Mapping

The Resources\Textures\Source\Static folder contains each of the JSON and PNG files that describe the available textures to randomize. These are loosely categorised for easy lookup. The main idea is that the JSON file for each source contains a map of colour names to a list of rectangles, and these rectangles map to areas on the associated PNG. The randomizer selects a colour and then extracts the bitmap from the PNG for each defined rectangle. The rectangles are stored in [X, Y, Width, Height] format.

Below is an extract from Resources\Textures\Source\Static\Enemies\Barney\Data.json.

{
  "VariantMap": {
    "Gold": [
      "240, 0, 64, 72",
      "240, 72, 64, 48"
    ],
    "Pink": [
      "320, 0, 64, 72",
      "320, 72, 64, 48"
    ]
  }
}

The Resources\Textures\Mapping folder contains details of what to include in each level, and the details of where each source rectangle (segment) should be positioned within the tiles. The source files are referenced by their folder structure within the static folder, but using dots in place of slashes.

Below is an extract from Resources\Textures\Mapping\WALL.TR2-Textures.json. This will tell the randomizer to change Barney's colour in Great Wall by mapping the source texture's first segment to tile 3 at position [136, 0], the second segment to tile 3 at [128, 208] and so on (Barney actually has 6 rectangles in total).

"Enemies.Barney": [
   {
     "Segment": 0,
     "Tile": 3,
     "X": 136,
     "Y": 0
   },
   {
     "Segment": 1,
     "Tile": 3,
     "X": 128,
     "Y": 208
   }
]

Grouping

The framework supports grouping sources together per level to allocate matching or similar colours. An example is in Floating Islands, so while the sources have been separated (islands, stone, lava etc) they are grouped together to maintain themes. The Floating Islands textures have also been carried through to Dragon's Lair.

Colours

The framework supports updating flat colours. An example of this in use is in updating the SkyBox colour to match the imported textures - see Resources\Textures\Source\Static\GreatWall\Sky\Data.json for example. The SkyBox entity ID is mapped here to a colour and a rectangle index in the source PNG. The colour defines the current TRColour4 used by the SkyBox's meshes, and the rectangle index is used to extract the new colour from the PNG (it takes the first pixel in the bitmap). The colour in the JSON is in [Red, Green, Blue] format.

"EntityColourMap": {
    "254": {
      "192, 224, 248": 0
    }
  }

So the above tells the framework to:

  • Extract the colour from pixel [0, 0] of the first bitmap of the selected source variant;
  • Add the new colour to Palette16 of the level if it doesn't already exist;
  • Find the meshes of entity ID 254 that use the colour with RGB [192, 224, 248];
  • Update each ColouredRectangle and ColouredTriangle of each mesh to use the new colour index.

Dynamic Sources and Mapping

An addition to the static PNG-to-level mapping is available by way of applying HSB operations directly to entire areas of the texture tiles. In Resources\Textures\Source\Dynamic are JSON files with objects that define various HSB values. In the level mapping files, a map of tile index to a group of rectangles within that tile can be defined. The result is that large portions of a level can be changed dramatically without having to store additional images. This is currently in use in the underwater and snow levels. The HSB values could in theory be completely randomized, but this will produce garish results, so for now there are currently 7 themes for the Randomizer to choose from for each of these level sets.

Persistent Textures

TextureRandomizer now has an option to use persistent textures. This means that individual texture groups will be randomized once, and then the same variant will be used in any other level that references it. For example, Lara's diving suit colour will be selected for 40 Fathoms, and then the same colour will appear in each of the other water levels. Note that texture grouping will always override persistent values to keep themes consistent.

Floating Islands

In addition to the TRGE update for the Floating Islands backup file, the new framework fully supports using Multipatch and EPC. The default FLOATING.TR2-Textures.json mapping file is used for Multipatch and EPC as the level file is identical for these versions, and there is an additional FLOATING.TR2-UKBox-Textures.json file for UK Box - the correct file is chosen based on the detected level file.

Layered Files

Included in Resources\Textures\Source\Static is LayeredTextures.zip. This contains all layered PNG (for Adobe Fireworks) and Photoshop files for all of the texture sources. This isn't included in the build so won't appear in the output folder.

Imported and Deleted Enemy Textures

The framework supports modifying enemy textures if they are imported into non-native levels. This is done by tracking where the individual segments are packed in the tiles during import. The source JSON files have a map of entity ID -> enemy segment ID -> source rectangle index. The segment ID is based on the index of the texture from the original level - see the ModelSegments folder within Resources\Textures\Source\Static\LayeredTextures.zip.

Resources\Textures\Source\Static\entity_lookup.json is used by the framework to map entity IDs to source folders.

If a level mapping file contains static mapping for enemies that have been removed from a level, these will be ignored.

Texture Maintenance

Currently, the various JSON and PNG files are managed manually. I'm going to work on a tool to allow texture editing so that the mapping, source rectangle definitions etc are easier to define. The existing TextureExport utility program has been updated with a few new options in the meantime. Running TextureExport /? will produce the following guide.

Usage: TextureExport [orig | gold | *.tr2] [png | html | segments]

Target Levels
        orig     - The original TR2 levels. Default option.
        gold     - The TR2 Golden Mask levels.
        *.tr2    - Use a specific level file.

Export Mode
        png      - Export each texture tile to PNG. Default Option.
        html     - Export all tiles to a single HTML document.
        segments - Export each object and sprite texture to individual PNG files.

The HTML files are useful for determining texture positions quickly. They also contain some other pieces of information, but I am aiming to collate all of this into a better tool - HTML is just an easy medium for now.

#124 Multiple Unarmed Locations

Multiple locations can now be defined as unarmed level weapon spots, so when randomizing, the location can be different each time. The locations are all within a certain area – see the table below. This is still handled in Resources\unarmed_locations.json but it may be better to move this to the standard zoning framework at a later date.

Unarmed Weapon Location Guide (spoilers)
Level Location Guide
Great Wall From the start to the second guardhouse (i.e. before room 4).
Venice From the start to the first floodgate (i.e. before room 10).
Bartoli's Hideout Anywhere in the water in the starting area.
Opera House From the start to the first window/glass shard room (i.e. before room 10).
Offshore Rig From the start up to and including the plane.
Diving Area From the start up to and including the first swinging hook room, but before "the other side" (room 42).
40 Fathoms From the first room inside the boat (9) and before the first trapdoor room (20).
Wreck of the Maria Doria From the start to the first block puzzle (room 47).
Living Quarters From the start to the piston room (0).
The Deck From the start, to the left and down into the pool, and then up to and including room 103.
Tibetan Foothills Between the start and before the jump that leads to the breakable ice (so up to and including room 81).
Barkhang Monastery From the start up to and including room 27 (so before you enter the Monastery).
Catacombs of the Talion Between the start and before the ledge after the slope jump (so before room 7).
Ice Palace Anywhere in the starting area - weapon is needed for the bell.
Temple of Xian Between the start and up to the main temple door in room 14.
Floating Islands Between the start, up to the main narrow island bridge (room 6), including the area to the left of the start.
Dragon's Lair Anywhere in the first two rooms.
Home Sweet Home Fixed location in closet.

Item Randomizer

Item randomization now takes place after enemy randomization. For unarmed levels, a score is calculated based on the enemy types to determine how difficult the level is. This is then used to decide how much additional ammo to give for the randomized weapon pickup. There is also now a 1/3 chance of the pistols being added in addition to the randomized weapon. This adds on to @Anopob's softlock fix for Dragon's Lair (#107). I'd appreciate checking the calculations here to see if the scoring is fair/accurate - see TR2RandomizerCore.Utilities.EnemyUtilities.GetEnemyDifficulty.

I don't believe there is an issue in moving item randomization after enemy randomization, but again would appreciate if this could be double checked.

The weapon and ammo in the HSH closet will also now be randomized, but similar to Ice Palace, the harpoon and grenade launcher are excluded. This is to avoid issues with the enemy kill count, and in case the enemy on the balcony doesn't have a weapon to break the glass.

Further to #113, if development mode is on, the pistols will be added to all possible unarmed locations.

Refactoring

TR2CombinedLevel combines the main data and the script data into one object and is used throughout the randomizers, so _levelInstance.Data and _levelInstance.Script replace the separate objects that were previously in place throughout.

LevelProcessor has been added and is now the parent of RandomizerBase - this handles the reading/writing of the levels, and provides common support for multithreading. TextureDeduplicator inherits from this, so this separation was made rather than making the deduplicator another Randomizer.

UI Updates

  • Added an option to disable demos to prevent spoilers showing in the title screen.
  • Added TaskBarItemInfo progress bar for the main randomization process.
  • Added a spinner icon to progress dialogs.
  • Further to #113, a warning is displayed before randomization starts if development mode is on.
  • #125 Fixes a problem that was preventing the recent folder history list from displaying in the initial UI screen.

Misc

  • Additional sounds are categorised as potential secret sounds in Resources\tr2audio.json.
  • A couple of JSON formatting issues in the zone files for Rig and LQ have been fixed.

Key Items

  • Randomization of key items is now supported.
  • Some exceptions are made where there are pickups triggers below a key item (Deck + Catacombs - will be resolved in a future release).
  • For Barkhang, keys are not randomized - only prayer wheels and gemstones. It is thought that randomizing absolutely every key item on Barkhang would be too tedious and potentially ruin the enjoyment of the level.