Repository navigation
Conversation
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
force-pushed
the
fix/ledge-hang-and-glyphs
branch
from
October 14, 2026 10:23
2194bb0 to
d885ace
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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
roomAbovelink.Collisionhandles this:Collision::getFloor()walks up to the room at the sample height. But three places calledgetFloorInfo(getRoomIndex(), ...)directly:updateVelocity): the ledge slope is taken from the alcove, which is flat, sovelocity.ywas 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.getStateHang): it read the alcove's headroom (2559) instead of the ledge's (about 520). So the climb was allowed, and Lara got stuck.STATE_REACH): it read the ceiling from the same wrong room.b) The steep slant check did not look at the slope direction.
Collision::checkHeighttreats any sector withabs(slantX) > 2 || abs(slantZ) > 2as a wall whenmaxAscent == 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_floor2vsright_floor2againstSLOPE_DIFinLaraHangTest.c) A blocked shimmy could keep moving sideways.
When the front sample fails inside the sector Lara hangs on,
Collisionpushes out withd[q] = getOffset(...), which is 0 inside the same sector. So the step along the shimmy axis is kept. Meanwhilemove()resets the animation toANIM_HANGevery frame. Result: Lara slides without animation.d) On grab, the ledge height was sampled 128 units inside the ledge.
The grab
CollisionusesgetDir() * 128as 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 atLARA_RADIUS + 32, then saw the ledge about 100 units below the hands and blocked.Fix
Lara::getLedgeInfo(p, ledge, info). It first finds the room at ledge height (ledge - 32) above Lara's position withCollision::getFloor(), asCollisiondoes. Then it reads the floor info atp, keeping the existingroomAbovefallback. It is used for the hang velocity, the hang up check and the grab.slantAxisargument toCollision. 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.pos.x/z = opos.x/z) together with the existingANIM_HANGreset.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.

Root cause
UI::patchGlyphs()builds the extra glyph sprites with the spriteTR::TextureInfoconstructor. That constructor leavesindex,clut,tile,animated,texCoord[2..3],texCoordAtlas[2..3],subandi5uninitialized.Level::initTextures()skips sprites witht.tile == 0xFFFF. When the uninitializedtilehappens to be0xFFFF, that glyph is never packed into the glyph atlas. ItstexCoordAtlaskeeps 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::LoadBMPset r/g/b to 255 for every pixel and are freed at the end ofinitTextures(). Memory reused from those buffers is mostly0xFFbytes, sotile == 0xFFFFbecomes more likely on each reload.Fix
Initialize all fields in the sprite
TextureInfoconstructor, withtile = 0. The extra glyphs are not taken from a level texture tile;fillCallbackreads them from the glyph bitmaps by sprite id. This constructor is only used bypatchGlyphs().Testing
LARA_HEIGHT.🤖 Generated with Claude Code