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

PTH Format (Path Network)

A .pth file is an area’s pathfinding graph: a flat list of waypoint-like points and a flat list of directed connections between them. The engine walks it to generate movement successors when a creature needs a route across a room. It is authored per area and shares the area’s resref.

At a Glance

PropertyValue
Extension(s).pth
Magic SignaturePTH / V3.2
TypePathfinding node graph
Rust ReferenceNo typed view. Read as a generic GFF tree via rakata_formats::Gff.

Population: every .pth in the RIM archives under modules/, which is 132 files across 117 modules. The type occurs nowhere else in a retail install: not in the BIFs indexed by chitin.key, not in rims/, not in lips/, not in patch.erf.

The network is two-dimensional and the world supplies the third dimension

A path point stores X and Y. There is no Z label in any shipped file, the loaded point has no third coordinate, and everywhere the engine needs a point as a 3D position it fills the height in with zero.

This is a design decision rather than a gap in the data. Those flattened positions go straight to the walkmesh line tests, which follow the surface, so a point sits at whatever height the walkmesh under it happens to be. The path network says where you may walk in plan view. The walkmesh answers how high that is.

Two things follow. If you are reading these files you are not missing a field, so do not go hunting for one or work a height out from nearby geometry. And if you are writing them you have no height to supply, which means a path network is only as good as the walkmesh beneath it. Put a point over a hole in the walkmesh and you have not placed it at the wrong height. You have placed it somewhere the line tests cannot reach at all.

Warning

Destination is an unbounded index, so the bounds check is yours A connection’s Destination goes straight into the point array as an index, and nothing anywhere in that routine compares it against the number of points first. Put a value past the end and the engine reads whatever is sitting there. It does not reject the file.

This is the same shape as the walkmesh edge transition, which indexes the area’s room array on the same terms; see “A non-sentinel transition is used as a room index with no bounds check”. The difference is that walkmesh transitions have -1 to mean “nowhere” and path connections have no sentinel at all, so there is no value that safely means “no destination”. Every Destination must be a valid index into that file’s own Path_Points, unconditionally.

Every Destination in every shipped file is in range, so nothing in a retail install exercises this.

Warning

The connection list is spelled Path_Conections, with one n So is the per-point Conections count. The misspelling is in the format, and it is the single most likely thing here to cost you an afternoon: a reader that asks for Path_Connections finds nothing and concludes the file has no connections, which looks exactly like a path network that genuinely has none.

File Layout

Two sibling lists at the root, and nothing else.

LabelElement
Path_PointsA point: position, plus a span naming its outgoing connections
Path_ConectionsA connection: the index of the point it leads to

A point does not carry its own connections. It carries an offset and a count into the shared connection array, so the two lists are read together or not at all.

Field table

Every label this format declares is read by the loader. There are no unread fields in it, and no field the loader asks for that no shipped file writes.

LabelGFF typeAbsent-value defaultNotes
Path_PointsListemptyPresent in every shipped file; empty in 38 of them
Path_Points[].XFLOAT0.0Stored verbatim, no arithmetic at load
Path_Points[].YFLOAT0.0Stored verbatim, no arithmetic at load
Path_Points[].First_ConectionDWORD0Index of this point’s first connection
Path_Points[].ConectionsDWORD0How many connections belong to this point
Path_ConectionsListemptyNot read at all when Path_Points is empty. See below.
Path_Conections[].DestinationDWORD0Index into Path_Points. Not bounds-checked.

Rules the engine enforces

Both lists are gated on their elements’ struct ids: 2 for Path_Points, 3 for Path_Conections. The struct_id is not a free tag here. The loader tests each element’s against the value its list expects and skips the element outright when it differs, silently, moving to the next index. A point with the wrong id is not in the network, and a connection with the wrong id leaves its point’s span short, which matters more than it sounds given the span arithmetic below assumes the array tiles exactly.

This is the same gate GIT puts on its object lists, and it turned up from the same sweep, but a .pth is its own resource rather than anything nested in an area.

First_Conection and Conections are a genuine offset and count. The successor step computes the end of the span as first plus count and walks the connection array between them, so the pair indexes a shared flat array exactly as it looks. This holds on both sides: measured across all 94 shipped files with a populated point list, walking points in file order, each point’s First_Conection equals the running total of the preceding counts and the last span ends precisely at the connection array’s length. The array is fully tiled with no unreferenced entries and no overlap.

Note

An empty path network is a supported state, not a broken file 38 of the 132 shipped files carry an empty Path_Points, and the loader treats that as a clean exit: it records a point count of zero, allocates nothing, and never reads Path_Conections at all even though that list is a sibling it would otherwise read unconditionally. The caller discards the loader’s result outright, so nothing downstream distinguishes “pathing loaded”, “pathing was empty” and “there was no .pth”.

Two consequences. A module with no path network is untested rather than degraded. And a file with an empty point list may carry any connection data at all without effect, because nothing will look at it.

Engine Audits & Decompilation

Read from CSWSArea::LoadPathPoints at 0x00508400 in swkotor.exe, with the consumer traced separately. Provenance: traced, and the file-layout claims are independently measured against the module archives.

Pipeline EventEngine Behaviour
Existence gateBefore anything else the loader asks the resource manager whether a PTH-type resource exists under the area’s own resref. If not, the whole call is a no-op.
Module-scoped, genuinelyThe GFF it opens is keyed on the area’s own resref rather than a fixed name, so a module’s own .pth is the one that loads. (Contrast JRL, where the equivalent lookup is hardcoded and module files are inert.)
Verbatim storageBoth lists are read field by field into the loaded structures with no arithmetic, validation or normalisation at load time. Everything interesting happens at consumption.
Result discardedCSWSArea::LoadArea (0x0050e1e3) calls the loader once and ignores its return value, which is why a missing or empty network is silent.
ConsumptionCSWSArea::PathPointDFSGenerateSuccessors (0x004bdb00) is where the offset-and-count walk and the unchecked Destination subscript both live. The 3D positions it produces feed the walkmesh line tests documented on the walkmesh page.

Implemented Linter Rules (Rakata-Lint)

None yet. No rule currently reads this format.