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
| Property | Value |
|---|---|
| Extension(s) | .pth |
| Magic Signature | PTH / V3.2 |
| Type | Pathfinding node graph |
| Rust Reference | No 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
Destinationis an unbounded index, so the bounds check is yours A connection’sDestinationgoes 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-sentineltransitionis used as a room index with no bounds check”. The difference is that walkmesh transitions have-1to mean “nowhere” and path connections have no sentinel at all, so there is no value that safely means “no destination”. EveryDestinationmust be a valid index into that file’s ownPath_Points, unconditionally.Every
Destinationin every shipped file is in range, so nothing in a retail install exercises this.
Warning
The connection list is spelled
Path_Conections, with onenSo is the per-pointConectionscount. The misspelling is in the format, and it is the single most likely thing here to cost you an afternoon: a reader that asks forPath_Connectionsfinds 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.
| Label | Element |
|---|---|
Path_Points | A point: position, plus a span naming its outgoing connections |
Path_Conections | A 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.
| Label | GFF type | Absent-value default | Notes |
|---|---|---|---|
Path_Points | List | empty | Present in every shipped file; empty in 38 of them |
Path_Points[].X | FLOAT | 0.0 | Stored verbatim, no arithmetic at load |
Path_Points[].Y | FLOAT | 0.0 | Stored verbatim, no arithmetic at load |
Path_Points[].First_Conection | DWORD | 0 | Index of this point’s first connection |
Path_Points[].Conections | DWORD | 0 | How many connections belong to this point |
Path_Conections | List | empty | Not read at all when Path_Points is empty. See below. |
Path_Conections[].Destination | DWORD | 0 | Index 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 readsPath_Conectionsat 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 Event | Engine Behaviour |
|---|---|
| Existence gate | Before 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, genuinely | The 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 storage | Both 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 discarded | CSWSArea::LoadArea (0x0050e1e3) calls the loader once and ignores its return value, which is why a missing or empty network is silent. |
| Consumption | CSWSArea::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.