Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

LYT (Layout File)

LYT files are ASCII configuration arrays that define the spatial 3D placement and orientation of independent room models to construct a complete area map.

At a Glance

PropertyValue
Extension(s).lyt
Magic SignatureNone
TypePlain Text Layout
Rust ReferenceView rakata_formats::Lyt in Rustdocs

File Structure

Four count-led sections between a beginlayout and donelayout pair. Each section declares how many entries follow, and the entries are whitespace-separated fields on their own lines. Text is Windows-1252. Counted throughout, with no terminator on any run: see counted, terminated, or neither for why guessing the wrong one is not a graceful failure.

beginlayout
  roomcount <N>
    <room_model> <x> <y> <z>
  trackcount <N>
    <track_model> <x> <y> <z>
  obstaclecount <N>
    <obstacle_model> <x> <y> <z>
  doorhookcount <N>
    <room_name> <door_name> <reserved> <x> <y> <z> <qw> <qx> <qy> <qz>
donelayout
SectionFields per entryMeaning
roomcountmodel, then x y zA room model and where to place it
trackcountmodel, then x y zSwoop track geometry
obstaclecountmodel, then x y zSwoop obstacle geometry
doorhookcountroom, door, reserved, x y z, then qw qx qy qzWhere a door attaches, with a quaternion orientation rather than a bare position

Doorhook entries

The doorhook reserved field sits third, between the two names and the position, and it is an integer written as 0. That is its value in every doorhook of every .lyt a retail install ships, so a writer emits 0. It cannot be omitted: the fields are positional and whitespace-separated, so dropping it shifts x into its place and the whole entry reads wrong.

Important

The doorhook quaternion is w first The four floats after the position are w, x, y, z, the same order as GFF’s Vector4 and the MDL quaternion. Decode them as x, y, z, w and you write back a different orientation than you read.

A door hook is a yaw about the vertical axis, so x and y are zero in all but eight rows across a retail install. Many rows read exactly 1.0 0.0 0.0 0.0, the identity, and the yaws that remain land on multiples of 90 degrees. The eight exceptions sit on angled geometry and carry a populated y or all four components.

Read the same rows as x, y, z, w and the zeroes move to the first two slots for one subset only, the half turns, where w is genuinely 0. That covers a minority of the rows. Reading w first covers all but the eight.

Measured over every .lyt indexed by a retail chitin.key and every doorhook row in them. Provenance: measured.

Warning

The engine stores every doorhook field and reads none of them back CLYT::LoadLayout parses each doorhook line into five live arrays: room name, door name, the integer third field, the position and the orientation. Nothing reads them back, so no door is linked to a room through this structure. See what can reach the doorhook arrays.

Write the section anyway. Every .lyt a retail install ships declares it, and the ones with no hooks say so with doorhookcount 0 followed by donelayout.

Keep the count matching the rows that follow it. No shipped file disagrees, so the engine’s behaviour on a mismatch is untested, the parse loop does not verify its own terminator (see Boundary Oversight below), and the arrays are sized from the declared count before anything else can intervene.

Omitting the section is survivable but pointless: the parser meets donelayout, never reaches the branch that reads the count, and leaves it at zero.

Reports of memory corruption in modules with the doorhook section stripped come from the parse. Nothing is reading these values back.

Section order

The section order is not a convention. The engine’s parser walks them sequentially, and a file presenting them out of order will not load.

Note

The four sections cascade, and only the tail is optional The parser reads roomcount the moment it finds beginlayout, with no donelayout test first, so you can never skip the room section.

After that it tests each following line against the literal donelayout before treating it as the next section’s count. A match ends the file there and the remaining sections go unread. A non-match means the parser takes that line as trackcount N whatever it actually says, then runs the same test before obstaclecount and again before doorhookcount.

So a file may stop early, and shipped layouts do exactly that in effect by declaring the later sections empty. What a file may not do is skip a section in the middle: drop trackcount while keeping obstaclecount and the parser reads the obstacle count as the track count, putting everything after it in the wrong section.

A count keyword followed by no number is a third case. The read is an unchecked sscanf: the conversion fails, writes nothing, and returns a value nobody examines, so the count keeps what it already held. Every caller supplies a fresh instance, so that is zero.

Provenance: traced.

Unchecked counts

Warning

No count in this format is bounds-checked before it sizes an allocation All four sections behave alike. roomcount, trackcount, obstaclecount and doorhookcount are each read as a plain integer and multiplied into a byte size, with no upper-bound check anywhere in the loader. The parse loop then writes that many elements into whatever the allocation turned out to be.

A large enough count overflows the multiplication into an allocation smaller than the loop is about to fill. This belongs to the layout parser as a whole rather than to any one section, and it holds whether or not anything downstream reads the values.

Provenance: traced, as far as the missing check. What a given oversized count does past that point was not followed.

Where Rakata differs

Note

Rakata reads more loosely than the engine does The engine skips everything before beginlayout, which is what makes the ubiquitous #MAXLAYOUT ASCII preamble harmless. Our parser goes further and ignores any line it does not recognise as a known count section, wherever it appears, so files carrying comments or dependency metadata between sections still parse. That is deliberate tolerance on the read side; anything written back out is canonical.

Engine Audits & Decompilation

Read from CLYT::LoadLayout at 0x005de900 in swkotor.exe. Individual claims below carry their own provenance where it is known. Rows with no marker are derived, not attested, and sit on the reverse-engineering queue.

Pipeline EventGhidra Provenance & Engine Behavior
Newline BoundsThe parser heavily expects explicit \r\n (CRLF) endings. Scanning extracts target strings utilizing _sscanf("%[^\r\n]", ...) patterns and frequently relies on blind +2 byte pointer leaps to manually clear the terminators.
Preamble SkippingAll file lines existing prior to the beginlayout execution marker (such as the ubiquitous #MAXLAYOUT ASCII header) are deliberately skipped and ignored.
Sequential ParsingThe structure mandates a rigid sequential ingestion. Data collections must explicitly appear geographically in the exact order: roomcounttrackcountobstaclecountdoorhookcountdonelayout.
Doorhook data is stored and never readProvenance: traced. Parsed into five live arrays that nothing reads back. Enumerated in full below.

Warning

Boundary Oversight While the engine systematically verifies donelayout boundaries separating the primary collections, the underlying parse loop functionally neglects to verify the final donelayout signature upon closing the doorhooks segment.

What can reach the doorhook arrays

Each doorhook line is parsed with a single sscanf into five separate arrays. The integer third field gets an array of its own, so the parser retains it rather than discarding it.

CLYT exposes getters for room, track and obstacle data and none for any doorhook field. CLYT::LoadLayout has two callers, CSWSArea::LoadRoomInfo (0x005073d0) and CSWCArea::LoadArea (0x00607610). Neither references the doorhook arrays, and CLYT::UnloadLayout (0x005de450) releases the resource without reading them.

Nothing reaches the arrays indirectly either. CLYT offers no path a caller could dispatch through, and the instance never outlives the call that loads it.