Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@CromFr
Last active June 16, 2018 10:28
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save CromFr/104bac52dc9191099d9d9e3dbd2c4975 to your computer and use it in GitHub Desktop.
Save CromFr/104bac52dc9191099d9d9e3dbd2c4975 to your computer and use it in GitHub Desktop.

NWN2 TRN.ASWM packet structure

Thibaut CHARLES - CromFr@gmail.com

This document is a mix of information:

  • I found by reverse engineering: basic structure, vertices, edged, triangles
  • Extracted from Skywing NWN2DataLib source code: everything else

My implementation can be found here, and supports parsing, serialization, mesh importation, path table & island baking.

Packet

TRN.ASWM packets are used to store the walkmesh information in TRN and TRX files.

The packet itself is stored as is:

Type Name Description
char[4] data_type Always COMP for compressed walkmesh
uint32_t compressed_data_size Size of compressed_data
uint32_t uncomp_length Uncompressed walkmesh size
void[compressed_data_size] compressed_data zlib-compressed walkmesh data

Uncompressed walkmesh data can be retrieved using zlib.deflate. The following structure information are for the uncompressed walkmesh data.

TRN/TRX differences

TRN.ASWM is slightly different weather it is stored in a TRX or TRN file. Generally, the TRX version is smaller as it has to be downloaded/loaded by the client, and contains the baked version of the walkmesh (with placeable walkmesh/walkmesh cutter alterations).

  • TRN version:
    • The whole map is stored, but no information on placeable walkmeshes nor walkmesh cutters
    • Triangle clockwise flag is not set
    • Contains map border geometry
    • Path tables & island list are empty
  • TRX version:
    • Baked counterpart of the TRN version
    • Map borders are not stored, only the center.
    • Triangles cut by a "walkmesh cutter" are removed (creates holes in the mesh)
    • Placeable walkmesh modifications appears
    • Path tables & island list are populated

Data blocks

Type Name d
Header header
Vertex[header.vertices_count] vertices
Edge[header.edges_count] edges
Triangle[header.triangles_count] triangles
TilesHeader tiles_header
Tile[tiles_header.grid_height * tiles_header.grid_width] tiles
uint32_t tiles_border_size Width of the map borders in tiles
uint32_t islands_length Length of islands
Island[islands_length] islands
IslandPathNode[islands_length * islands_length] islands_path_nodes Pathfinding data

Structures

Header struct (53 bytes)

Type Name Description
uint32_t version
char[32] name
uint8_t owns_data
uint32_t vertices_count
uint32_t edges_count
uint32_t triangles_count
uint32_t triangles_offset Seems to be always 0

Vertex struct (12 bytes)

Type Description
float[3] x/y/z coordinates of the vertex

Edge struct (16 bytes)

Type Description
uint32_t[2] Vertex indices
uint32_t[2] Attached triangle indices. -1 if a triangle does not exist.
Notes
  • The struct defines a edge between two triangles, with the shared edge.
  • Every triangle has 3 Edge structs.
  • Probably only used to ease path tables generation
  • TODO: I should try remove all Edges from a baked trx and see what happens

Triangle struct (64 bytes)

Type Name Description
uint32_t[3] vertices Vertex indices composing the triangle
uint32_t[3] linked_edges Edge indices corresponding to this triangle edges
uint32_t[3] linked_triangles Triangle indices that have an edge in common with this triangle. -1 is there is no triangle on one.
float[2] center Triangle center x/y coordinates
float[3] normal Normal vector
float dot Dot product at plane
uint16_t island -1 if the triangle is non walkable, else it is an island index
uint16_t flags Walkmesh flags
  • linked_triangles[i] and linked_edges[i] must share the same edge
Triangle flags
  • walkable = 0x01: if the triangle can be walked on
  • clockwise = 0x04: vertices are wound clockwise and not ccw
  • dirt = 0x08: Floor type (for sound effects)
  • grass = 0x10: Floor type (for sound effects)
  • stone = 0x20: Floor type (for sound effects)
  • wood = 0x40: Floor type (for sound effects)
  • carpet = 0x80: Floor type (for sound effects)
  • metal = 0x100: Floor type (for sound effects)
  • swamp = 0x200: Floor type (for sound effects)
  • mud = 0x400: Floor type (for sound effects)
  • leaves = 0x800: Floor type (for sound effects)
  • water = 0x1000: Floor type (for sound effects)
  • puddles = 0x2000: Floor type (for sound effects)
island & walkable flag
  • In TRN files:
    • Map border megatiles triangles have island=-1 and walkable flag set to 0
    • Bakeable triangles (map center) have a valid island (ie: != -1), and the walkable flag set to 1/0 if the triangle is meant to be walkable/non walkable.
  • In TRX files:
    • Map borders are not stored
    • Non walkable triangles have island=-1 and walkable flag set to 0
    • Triangles cut by a "walkmesh cutter" are not stored
Notes
  • For the raw map:
    • unknownA is always 0 (or -0)
  • For the icy peak map
    • -498.294 ==> 475.229

TilesHeader struct (20 bytes)

Type Name Description
uint32_t flags Always 31 in TRX files, 15 in TRN files
float width Width in meters of a terrain tile (most likely to be 10.0)
uint32_t grid_height Number of tiles along Y axis
uint32_t grid_width Number of tiles along X axis
uint32_t border_size Width of the map borders in tiles (8 means that 8 tiles will be removed on each side)

Tile struct (variable length)

A tile is a square (generally 10 x 10 meters) containing mesh triangles. In TRX files, the tiles also contains pre-calculated pathfinding information.

Type Name Description
TileHeader header
Vertex[] vertices Unused by nwn2. Might be usable with tile.header.owns_data == true
Edge[] edges Unused by nwn2. Might be usable with tile.header.owns_data == true
PathTableHeader path_table_header
ubyte[] local_to_node local_to_node[localTriangleIndex] is an index in nodes
uint32_t[] node_to_local node_to_local[nodeValue] is a local triangle index
ubyte[] nodes nodes[node_to_local_length * fromLTNIndex + destLTNIndex] is an index in node_to_local
uint32_t flags Reuse value aswm.tiles_header.tiles_flags
  • All triangles of a tile must have consecutive indices
  • The triangle owned by a tile can be retrieved using aswm.triangles[header.triangles_offset .. header.triangles_offset + header.triangles_count]
  • A tile must not re-use triangles owned by another tile (will crash NWN2)
  • TODO: I need to try making non-square tiles and see how the game handles it

Tile path table

This is a pre-calculated pathfinding table, that reference the next triangle to go to in order to reach any other triangle of the tile.

This is how a path on a tile is calculated:

  1. Get local triangle indices:
    • fromLocIdx = fromGlobIdx - tile.header.triangles_offset for the starting triangle
    • destLocIdx = destGlobIdx - tile.header.triangles_offset for the triangle you are trying to reach
    • If fromLocIdx or destLocIdx equals 255, it means the triangle is not walkable
  2. Get node indices:
    • fromNodeIdx = tile.local_to_node[fromLocIdx]
    • destNodeIdx = tile.local_to_node[destLocIdx]
  3. Get the Node value
    • node = tile.nodes[fromNodeIdx * header.node_to_local_length + destNodeIdx]
    • node == 255 can mean two things:
      • Destination cannot be reached, because one of the triangles is non- walkable or there is an obstacle that can't be get around without leaving the tile.
      • If fromNodeIdx == destNodeIdx, this means you already reached the destination triangle
  4. Get the next local triangle index to go to in order to reach destination
    • nextLocIdx = tile.node_to_local[node & 0b0111_1111]
  5. Loop on 2. with fromNodeIdx = nextLocIdx, until nextLocIdx == destNodeIdx

Notes:
  • nodes[i] & 0b1000_0000 is a bit flag for if there is a clear line of sight between the two triangle. It's not clear what LOS is since two linked triangles on flat ground may not have LOS = 1 in game files. A value of 0 means the game will have to calculate LOS in real time.

TileHeader struct (57 bytes)

Type Name Description
char[32] name Sometime contains garbage, sometime contains only null values
ubyte owns_data 1 if the tile stores vertices / edges. NWN2 always set this to 0
uint32_t vertices_count Number of vertices in this tile
uint32_t edges_count Number of edges in this tile
uint32_t triangles_count Number of triangles in this tile (walkable + unwalkable)
float size_x TODO: Always 0?
float size_y TODO: Always 0?
uint32_t triangles_offset

PathTableHeader struct (13 bytes)

Type Name Description
uint32_t compression_flags Always 0. Note: it's pointless to compress path table since the aswm is already compressed
uint32_t local_to_node_length Length of local_to_node array
ubyte node_to_local_length Length of node_to_local array
uint32_t rle_table_size Always 0.
  • compression_flags values: rle = 1, zcompress = 2

Island struct (variable length)

An island is a fraction of a tile, where all walkable triangles can be reached without leaving the tile. This is a pathfinding related struct that is not available in TRN files.

Type Name Description
IslandHeader header
uint32_t linked_islands_length Length of linked_islands
uint32_t[] linked_islands Island indices linked to this one
uint32_t linked_islands_dist_length Length of linked_islands_dist
float[] linked_islands_dist Distance to linked islands
uint32_t linked_islands_exit_length Length of linked_islands_exit
uint32_t[] linked_islands_exit Exit triangles leading to linked islands
  • linked_islands_dist[i] and linked_islands_exit[i] are the distance & exit to linked_islands[i]
  • I don't see the point of storing 3 times the same length. Maybe the nwn2 devs did not have time clean this.

IslandHeader struct (24 bytes)

Type Name Description
uint32_t index Index of the island in the aswm.islands array
uint32_t tile Value looks pretty random, but is identical for all islands. TODO: try setting it to associated tile index
float[3] center Center of the island. Z is always 0
uint32_t triangles_count Number of triangles in this island

IslandPathNode struct (8 bytes)

Pathfinding across islands works very similarly to Tile path table.

aswm.islands_path_nodes[fromIslandIdx * aswm.islands_length + toIslandIdx] is the next island to go to in order to reach toIslandIdx from fromIslandIdx

Type Name Description
uint16_t next Next island to go to
uint16_t _padding unused
float weight Distance to the next island
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment