Skip to content

Fix ledge hanging on stacked rooms / steep ledges and missing extra glyphs - #539

Open
johnhsb wants to merge 2 commits into
XProger:masterfrom
johnhsb:fix/ledge-hang-and-glyphs
Open

johnhsb wants to merge 2 commits into
XProger:masterfrom
johnhsb:fix/ledge-hang-and-glyphs

Conversation

@johnhsb

@johnhsb johnhsb commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

This PR fixes three ledge hanging problems and one text rendering problem. The causes were found by decoding savegames taken at the failing spots and checking the original TR1 level data (floor data, room stacking and animation commands).

1. Ledge hanging

Symptoms

  • Shimmy stops for no visible reason. Seen in Palace Midas, room 44 around (45027, -5930, 57444): the ledge goes down while the ceiling above it goes up. Lara stops after a few units, and the shimmy animation restarts every frame.
  • Lara can climb up under a ceiling lower than her height, and then gets stuck. Same spot: the gap between the ledge and the ceiling is about 520, but hang up is allowed. After the climb, Lara's head is inside the ceiling and she cannot move in any direction.
  • No shimmy at all on ledges that are steep only towards/away from Lara. Seen in Sanctuary of the Scion, room 34 around (37268, 978, 33692): the ledge top is a 3-click slope in front of Lara and flat along the wall.
  • Lara slides sideways without animation on some steep ledges when the shimmy is blocked.
Ledge Palace Midas Ledge Sanctuary Scion

Root causes

a) The ledge was read from Lara's own room instead of the room that holds the ledge.
While hanging, Lara's position (her body) is about 724 units below her hands. The ledge can belong to a room stacked above her room. At the Palace Midas spot, Lara is in room 44 and the ledge is in room 45. Lara's room can also have a sector under the ledge block that has nothing to do with the ledge. Room 44 has an alcove there (floor -2048, ceiling -4096) with no roomAbove link.

Collision handles this: Collision::getFloor() walks up to the room at the sample height. But three places called getFloorInfo(getRoomIndex(), ...) directly:

  • Hang velocity (updateVelocity): the ledge slope is taken from the alcove, which is flat, so velocity.y was 0 on a sloped ledge. The hands stayed at the same height while the real ledge went down. Collision, which reads the correct room, then saw the ledge more than 32 units below its sample point and blocked the shimmy.
  • Hang up check (getStateHang): it read the alcove's headroom (2559) instead of the ledge's (about 520). So the climb was allowed, and Lara got stuck.
  • Swing check on grab (STATE_REACH): it read the ceiling from the same wrong room.

b) The steep slant check did not look at the slope direction.
Collision::checkHeight treats any sector with abs(slantX) > 2 || abs(slantZ) > 2 as a wall when maxAscent == maxDescent. The hang shimmy uses that mode, so a ledge that is steep only towards/away from Lara blocked the shimmy, even though it is flat along the wall. The original game checks only the floor difference along the wall while hanging: left_floor2 vs right_floor2 against SLOPE_DIF in LaraHangTest.

c) A blocked shimmy could keep moving sideways.
When the front sample fails inside the sector Lara hangs on, Collision pushes out with d[q] = getOffset(...), which is 0 inside the same sector. So the step along the shimmy axis is kept. Meanwhile move() resets the animation to ANIM_HANG every frame. Result: Lara slides without animation.

d) On grab, the ledge height was sampled 128 units inside the ledge.
The grab Collision uses getDir() * 128 as its offset. On a steep forward slope the floor there is much higher than at the edge: 96 units for a 3-click slope. So the hands floated above the edge. The shimmy checks, which sample at LARA_RADIUS + 32, then saw the ledge about 100 units below the hands and blocked.

Fix

  • Add Lara::getLedgeInfo(p, ledge, info). It first finds the room at ledge height (ledge - 32) above Lara's position with Collision::getFloor(), as Collision does. Then it reads the floor info at p, keeping the existing roomAbove fallback. It is used for the hang velocity, the hang up check and the grab.
  • Add an optional slantAxis argument to Collision. When it is set, only the slope along that axis counts as steep. move() passes the wall direction (cos(angle.y), -sin(angle.y)) while hanging. All other callers keep the old behaviour, which is the default.
  • When the hang shimmy is blocked, put Lara back at her previous position on X/Z (pos.x/z = opos.x/z) together with the existing ANIM_HANG reset.
  • On grab, sample the ledge height again at the edge (LARA_RADIUS + 2), and use it if it is within 256 of the first sample.

2. Extra glyphs (RU/JA/GR/CN/KO) missing or drawn as solid boxes

Symptom

After loading levels or savegames a few times, some wide glyphs in menus and the passport disappear or show as solid boxes. This is most visible with large glyph sets.
Extra glyphs missing

Root cause

UI::patchGlyphs() builds the extra glyph sprites with the sprite TR::TextureInfo constructor. That constructor leaves index, clut, tile, animated, texCoord[2..3], texCoordAtlas[2..3], sub and i5 uninitialized.

Level::initTextures() skips sprites with t.tile == 0xFFFF. When the uninitialized tile happens to be 0xFFFF, that glyph is never packed into the glyph atlas. Its texCoordAtlas keeps raw glyph page pixel coords (0..255), which the renderer reads as normalized coords (0..32767). So the glyph is drawn from a few texels in the atlas corner: blank, or a solid box when those texels are opaque.

It gets worse with reloads. The glyph RGBA buffers made by Texture::LoadBMP set r/g/b to 255 for every pixel and are freed at the end of initTextures(). Memory reused from those buffers is mostly 0xFF bytes, so tile == 0xFFFF becomes more likely on each reload.

Fix

Initialize all fields in the sprite TextureInfo constructor, with tile = 0. The extra glyphs are not taken from a level texture tile; fillCallback reads them from the glyph bitmaps by sprite id. This constructor is only used by patchGlyphs().

Testing

  • macOS (OpenGL) build.
  • Palace Midas, at the spot above: Lara follows the sloped ledge and shimmies to the end. Hang up is refused where the headroom is below LARA_HEIGHT.
  • Sanctuary of the Scion, at the spot above: Lara shimmies along the 3-click forward slope in both directions until she reaches the walls.
  • The menus and passport show all glyphs after loading savegames many times.

🤖 Generated with Claude Code

johnhsb and others added 2 commits October 14, 2026 19:23
Root causes:
- Hang code read the ledge from Lara's own room. When the ledge belongs to a
  room stacked above (e.g. Palace Midas room 44/45), Lara's room can have an
  unrelated sector under the ledge block, so the hang velocity ignored the
  ledge slope (hands drifted off the ledge until Collision blocked the shimmy)
  and the hang up check saw that sector's headroom (Lara climbed under a low
  ceiling and got stuck). Add getLedgeInfo() which resolves the room at ledge
  height first, as Collision does, and use it for hang velocity, hang up check
  and ledge grab.
- Collision treated a ledge slanted more than 2 clicks along any axis as a
  wall, so Lara could not shimmy along ledges that are steep only towards/away
  from her (e.g. Sanctuary of the Scion). While hanging only the slope along
  the wall is checked now (as in the original), via Collision::slantAxis.
- When the shimmy is blocked inside the sector Lara hangs on, Collision keeps
  the step along the shimmy axis, so Lara slid while her animation was reset
  to ANIM_HANG every frame. Cancel the sideways step.
- The ledge height was sampled 128 units inside the ledge when grabbing, so on
  steep slanted ledges the hands did not match the edge and the shimmy checks
  saw the ledge far below the hands. Re-sample it at the edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sprite TextureInfo constructor (used only by UI::patchGlyphs for the
RU/JA/GR/CN/KO glyphs) left tile, clut, index, animated and the remaining
texture coords uninitialized. When the garbage tile value happened to be
0xFFFF, initTextures skipped the glyph, so it was never packed into the glyph
atlas and kept raw page coords: the glyph was drawn from the atlas corner as
a blank or a solid box. The freed glyph conversion buffers are mostly 0xFF
bytes, so reloading levels made this increasingly likely (very visible with
the Korean glyphs). Initialize all fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@johnhsb
johnhsb force-pushed the fix/ledge-hang-and-glyphs branch from 2194bb0 to d885ace Compare October 14, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant