1 /// Terrain (trn, trx)
2 module nwn.trn.trn;
3 
4 import std.stdint;
5 import std.string;
6 import std.conv: to;
7 import std.traits;
8 import std.exception: enforce;
9 import std.algorithm;
10 import std.math;
11 import std.array: array;
12 import std.typecons: Tuple, tuple;
13 import nwnlibd.parseutils;
14 import nwnlibd.geometry;
15 import gfm.math.vector;
16 import gfm.math.box;
17 
18 public import nwn.trn.genericmesh;
19 public import nwn.twoda;
20 
21 import std.stdio: stdout, write, writeln, writefln;
22 version(unittest) import std.exception: assertThrown, assertNotThrown;
23 
24 ///
25 class TrnParseException : Exception{
26 	///
27 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
28 		super(msg, f, l, t);
29 	}
30 }
31 ///
32 class TrnTypeException : Exception{
33 	///
34 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
35 		super(msg, f, l, t);
36 	}
37 }
38 ///
39 class TrnValueSetException : Exception{
40 	///
41 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
42 		super(msg, f, l, t);
43 	}
44 }
45 ///
46 class TrnInvalidValueException : Exception{
47 	///
48 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
49 		super(msg, f, l, t);
50 	}
51 }
52 ///
53 class ASWMInvalidValueException : TrnInvalidValueException{
54 	///
55 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
56 		super(msg, f, l, t);
57 	}
58 }
59 ///
60 class TRRNInvalidValueException : TrnInvalidValueException{
61 	///
62 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
63 		super(msg, f, l, t);
64 	}
65 }
66 ///
67 class WATRInvalidValueException : TrnInvalidValueException{
68 	///
69 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
70 		super(msg, f, l, t);
71 	}
72 }
73 
74 ///Type of a packet's payload
75 enum TrnPacketType{
76 	NWN2_TRWH,/// TerrainWidthHeight
77 	NWN2_TRRN,/// Main terrain data
78 	NWN2_WATR,/// Water
79 	NWN2_ASWM,/// Zipped walkmesh data
80 }
81 ///
82 template TrnPacketTypeToPayload(TrnPacketType type){
83 	static if(type == TrnPacketType.NWN2_TRWH)      alias TrnPacketTypeToPayload = TrnNWN2TerrainDimPayload;
84 	else static if(type == TrnPacketType.NWN2_TRRN) alias TrnPacketTypeToPayload = TrnNWN2MegatilePayload;
85 	else static if(type == TrnPacketType.NWN2_WATR) alias TrnPacketTypeToPayload = TrnNWN2WaterPayload;
86 	else static if(type == TrnPacketType.NWN2_ASWM) alias TrnPacketTypeToPayload = TrnNWN2WalkmeshPayload;
87 	else static assert(0, "Type not supported");
88 }
89 ///
90 template TrnPacketPayloadToType(T){
91 	static if(is(T == TrnNWN2TerrainDimPayload))    alias TrnPacketPayloadToType = TrnPacketType.NWN2_TRWH;
92 	else static if(is(T == TrnNWN2MegatilePayload)) alias TrnPacketPayloadToType = TrnPacketType.NWN2_TRRN;
93 	else static if(is(T == TrnNWN2WaterPayload))    alias TrnPacketPayloadToType = TrnPacketType.NWN2_WATR;
94 	else static if(is(T == TrnNWN2WalkmeshPayload)) alias TrnPacketPayloadToType = TrnPacketType.NWN2_ASWM;
95 	else static assert(0, "Type not supported");
96 }
97 ///
98 TrnPacketType toTrnPacketType(char[4] str, string nwnVersion){
99 	return (nwnVersion~"_"~str.charArrayToString).to!TrnPacketType;
100 }
101 ///
102 char[4] toTrnPacketStr(TrnPacketType type){
103 	return type.to!(char[])[5 .. 9];
104 }
105 
106 
107 ///
108 struct TrnPacket{
109 
110 	/// Create an packet with default values
111 	this(TrnPacketType type){
112 		m_type = type;
113 
114 		typeswitch:
115 		final switch(m_type){
116 			static foreach(T ; EnumMembers!TrnPacketType){
117 				case T:
118 					alias PT = TrnPacketTypeToPayload!T;
119 					structData = new PT();
120 					break typeswitch;
121 			}
122 		}
123 	}
124 
125 	///
126 	this(TrnPacketType type, in ubyte[] payloadData){
127 		import std.traits: EnumMembers;
128 
129 		m_type = type;
130 
131 		typeswitch:
132 		final switch(type) with(TrnPacketType){
133 			static foreach(TYPE ; EnumMembers!TrnPacketType){
134 				case TYPE:
135 					alias PAYLOAD = TrnPacketTypeToPayload!TYPE;
136 					structData = new PAYLOAD(payloadData);
137 					break typeswitch;
138 			}
139 		}
140 	}
141 
142 	///
143 	this(T)(in T packet)
144 	if(is(T: TrnNWN2TerrainDimPayload) || is(T: TrnNWN2MegatilePayload) || is(T: TrnNWN2WaterPayload) || is(T: TrnNWN2WalkmeshPayload)){
145 		this(TrnPacketPayloadToType!T, packet.serialize());
146 	}
147 
148 	@property{
149 		///
150 		TrnPacketType type()const{return m_type;}
151 	}
152 	private TrnPacketType m_type;
153 
154 	/// as!TrnNWN2WalkmeshPayload
155 	ref inout(T) as(T)() inout if(is(typeof(TrnPacketPayloadToType!T) == TrnPacketType)) {
156 		assert(type == TrnPacketPayloadToType!T, "Type mismatch");
157 		return *cast(inout(T)*)structData;
158 	}
159 
160 	/// as!(TrnPacketType.NWN2_ASWM)
161 	ref inout(TrnPacketTypeToPayload!T) as(TrnPacketType T)() inout{
162 		return as!(TrnPacketTypeToPayload!T);
163 	}
164 
165 	/// Serialize a single TRN packet
166 	ubyte[] serialize() const {
167 		final switch(type) with(TrnPacketType){
168 			static foreach(TYPE ; EnumMembers!TrnPacketType){
169 				case TYPE:
170 					return as!TYPE.serialize();
171 			}
172 		}
173 	}
174 
175 private:
176 	void* structData;
177 }
178 
179 
180 
181 
182 
183 
184 /// TRN / TRX file parsing
185 class Trn{
186 	/// Empty TRN file
187 	this(){}
188 
189 	/// Parse a TRN file
190 	this(in string path){
191 		import std.file: read;
192 		this(cast(ubyte[])path.read());
193 	}
194 
195 	/// Parse TRN raw data
196 	this(in ubyte[] rawData){
197 		enforce!TrnParseException(rawData.length>Header.sizeof, "Data is too small to contain the header");
198 
199 		auto header =        cast(Header*)        rawData.ptr;
200 		auto packetIndices = cast(PacketIndices*)(rawData.ptr+Header.sizeof);
201 
202 		m_nwnVersion = header.file_type.charArrayToString;
203 		m_versionMajor = header.version_major;
204 		m_versionMinor = header.version_minor;
205 
206 		foreach(i ; 0..header.resource_count){
207 			immutable type = packetIndices[i].type.toTrnPacketType(nwnVersion);
208 			immutable offset = packetIndices[i].offset;
209 
210 			immutable packet = cast(immutable Packet*)(rawData.ptr+offset);
211 			immutable packetType = packet.type.toTrnPacketType(nwnVersion);
212 			immutable packetLength = packet.payload_length;
213 
214 			enforce!TrnParseException(type==packetType, "Packet type does not match the one referenced in packet indices");
215 
216 			packets ~= TrnPacket(type, (&packet.payload_start)[0..packetLength]);
217 
218 			version(unittest){
219 				auto ser = packets[$ - 1].serialize();
220 				assert(ser == (&packet.payload_start)[0..packetLength], format!"Mismatch on packet %d (%s)"(i, packetType));
221 			}
222 		}
223 
224 		version(unittest){
225 			assert(serialize() == rawData);
226 		}
227 	}
228 
229 	///
230 	ubyte[] serialize() const {
231 
232 		auto header = Header(
233 			m_nwnVersion.dup[0..4],
234 			m_versionMajor.to!uint16_t,
235 			m_versionMinor.to!uint16_t,
236 			packets.length.to!uint32_t);
237 
238 		PacketIndices[] indices;
239 		indices.length = packets.length;
240 
241 		uint32_t offset = (header.sizeof + PacketIndices.sizeof * indices.length).to!uint32_t;
242 		ubyte[] packetsData;
243 		foreach(i, ref packet ; packets){
244 			auto typeStr = packet.type.toTrnPacketStr();
245 
246 			indices[i].type = typeStr;
247 			indices[i].offset = offset;
248 			auto packetData = packet.serialize();
249 
250 			ChunkWriter cw;
251 			cw.put(typeStr, packetData.length.to!uint32_t, packetData);
252 			packetsData ~= cw.data;
253 			offset += cw.data.length;
254 		}
255 
256 
257 		ChunkWriter cw;
258 		cw.put(
259 			header,
260 			indices,
261 			packetsData);
262 		return cw.data;
263 	}
264 
265 
266 	///
267 	@property string nwnVersion()const{return m_nwnVersion;}
268 	///
269 	package string m_nwnVersion;
270 
271 	///
272 	@property uint versionMajor()const{return m_versionMajor;}
273 	///
274 	package uint m_versionMajor;
275 
276 	///
277 	@property uint versionMinor()const{return m_versionMinor;}
278 	///
279 	package uint m_versionMinor;
280 
281 	/// TRN packet list
282 	TrnPacket[] packets;
283 
284 	/// foreach(ref TrnNWN2WalkmeshPayload aswm ; trn){}
285 	int opApply(T)(scope int delegate(ref T packet) dlg)
286 	if(is(typeof(TrnPacketPayloadToType!T) == TrnPacketType)) {
287 		int res = 0;
288 		foreach(ref packet ; packets){
289 			if(packet.type == TrnPacketPayloadToType!T){
290 				if((res = dlg(packet.as!T)) != 0)
291 					return res;
292 			}
293 		}
294 		return res;
295 	}
296 
297 
298 private:
299 	static align(1) struct Header{
300 		static assert(this.sizeof == 12);
301 		char[4] file_type;
302 		uint16_t version_major;
303 		uint16_t version_minor;
304 		uint32_t resource_count;
305 	}
306 	static align(1) struct PacketIndices{
307 		static assert(this.sizeof == 8);
308 		char[4] type;
309 		uint32_t offset;
310 	}
311 	static align(1) struct Packet{
312 		static assert(this.sizeof == 8+1);
313 		char[4] type;
314 		uint32_t payload_length;
315 		ubyte payload_start;
316 	}
317 }
318 
319 
320 
321 /// Terrain dimensions (TRWH)
322 struct TrnNWN2TerrainDimPayload{
323 	uint32_t width;/// Width in megatiles
324 	uint32_t height;/// Height in megatiles
325 	uint32_t id;/// Unknown
326 
327 	/// Build packet with raw data
328 	this(in ubyte[] payload){
329 		width = *(cast(uint32_t*)&payload[0]);
330 		height = *(cast(uint32_t*)&payload[4]);
331 		id = *(cast(uint32_t*)&payload[8]);
332 	}
333 
334 	///
335 	ubyte[] serialize() const {
336 		ChunkWriter cw;
337 		cw.put(width, height, id);
338 		return cw.data;
339 	}
340 }
341 
342 /// Megatile information (TRRN)
343 struct TrnNWN2MegatilePayload{
344 	char[128] name;///name of the terrain
345 	///
346 	static align(1) struct Texture{
347 		static assert(this.sizeof == 44);
348 		char[32] name;
349 		float[3] color;/// rgb
350 	}
351 	Texture[6] textures;/// Textures on the megatile, with their blend color
352 	///
353 	static align(1) struct Vertex{
354 		static assert(this.sizeof == 44);
355 		float[3] position;/// x y z
356 		float[3] normal;  /// normal vector
357 		ubyte[4] tinting; /// BGRA format. A is unused.
358 		float[2] uv;/// Texture coordinates
359 		float[2] weights; /// XY1
360 	}
361 	Vertex[] vertices;/// Terrain geometry
362 	///
363 	static align(1) struct Triangle{
364 		uint16_t[3] vertices;///Triangle vertex indices in $(D TrnNWN2TerrainDimPayload.vertices)
365 	}
366 	/// Walkmesh grid triangles positions.
367 	/// Each uint16_t an index in `vertices` corresponding to a triangle vertex
368 	Triangle[] triangles;
369 	ubyte[] dds_a;/// 32 bit DDS bitmap. r,g,b,a defines the intensity of textures 0,1,2,3
370 	ubyte[] dds_b;/// 32 bit DDS bitmap. r,g defines the intensity of textures 4,5
371 	///
372 	static struct Grass{
373 		char[32] name;///
374 		char[32] texture;///
375 		///
376 		static align(1) struct Blade{
377 			static assert(this.sizeof == 36);
378 			float[3] position;///
379 			float[3] direction;///
380 			float[3] dimension;///
381 		}
382 		Blade[] blades;///
383 	}
384 	Grass[] grass;/// Grass "objects"
385 
386 	/// Build packet with raw data
387 	this(in ubyte[] payload){
388 		auto data = ChunkReader(payload);
389 
390 		name = data.read!(char[128]);
391 		//TODO: there is other data than name in this array
392 
393 		foreach(ref texture ; textures){
394 			texture.name = data.read!(char[32]);
395 		}
396 		foreach(ref texture ; textures){
397 			texture.color = data.read!(float[3]);
398 		}
399 		immutable vertices_length  = data.read!uint32_t;
400 		immutable triangles_length = data.read!uint32_t;
401 		vertices = data.readArray!Vertex(vertices_length).dup;
402 		triangles = data.readArray!Triangle(triangles_length).dup;
403 
404 		immutable dds_a_length = data.read!uint32_t;
405 		dds_a = data.readArray(dds_a_length).dup;
406 		immutable dds_b_length = data.read!uint32_t;
407 		dds_b = data.readArray(dds_b_length).dup;
408 
409 		immutable grass_count = data.read!uint32_t;
410 		grass.length = grass_count;
411 		foreach(ref g ; grass){
412 			g.name = data.read!(typeof(g.name)).dup;
413 			g.texture = data.read!(typeof(g.texture));
414 			immutable blades_count = data.read!uint32_t;
415 			g.blades.length = blades_count;
416 			foreach(ref blade ; g.blades){
417 				blade = data.readPackedStruct!(Grass.Blade);
418 			}
419 		}
420 
421 		enforce!TrnParseException(data.read_ptr == payload.length,
422 			(payload.length - data.read_ptr).to!string ~ " bytes were not read at the end of TRRN");
423 	}
424 
425 	///
426 	ubyte[] serialize() const {
427 		ChunkWriter cw;
428 		cw.put(name);
429 		foreach(ref texture ; textures)
430 			cw.put(texture.name);
431 		foreach(ref texture ; textures)
432 			cw.put(texture.color);
433 
434 		cw.put(
435 			vertices.length.to!uint32_t,
436 			triangles.length.to!uint32_t,
437 			vertices,
438 			triangles,
439 			dds_a.length.to!uint32_t, dds_a,
440 			dds_b.length.to!uint32_t, dds_b,
441 			grass.length.to!uint32_t);
442 
443 		foreach(ref g ; grass){
444 			cw.put(
445 				g.name,
446 				g.texture,
447 				g.blades.length.to!uint32_t, g.blades);
448 		}
449 
450 		return cw.data;
451 	}
452 
453 	/// Export terrain mesh to a `GenericMesh` struct
454 	GenericMesh toGenericMesh() const {
455 		GenericMesh ret;
456 
457 		ret.vertices.length = vertices.length;
458 		foreach(i, ref v ; vertices)
459 			ret.vertices[i] = vec3f(v.position);
460 
461 		ret.triangles.length = triangles.length;
462 		foreach(i, ref t ; triangles)
463 			ret.triangles[i] = GenericMesh.Triangle(t.vertices.to!(uint32_t[3]));
464 
465 		return ret;
466 	}
467 
468 	/// Check if the TRRN contains valid data
469 	void validate() const {
470 		import nwn.dds;
471 
472 		try new Dds(dds_a);
473 		catch(Exception e)
474 			throw new TRRNInvalidValueException("dds_a is invalid or format is not supported", __FILE__, __LINE__, e);
475 		try new Dds(dds_b);
476 		catch(Exception e)
477 			throw new TRRNInvalidValueException("dds_b is invalid or format is not supported", __FILE__, __LINE__, e);
478 
479 		foreach(vi, v ; vertices){
480 			enforce!TRRNInvalidValueException(v.position[].all!"!a.isNaN",
481 				format!"vertices[%d] has an invalid position: %s"(vi, v));
482 		}
483 
484 		immutable vtxLen = vertices.length;
485 		foreach(ti, ref t ; triangles){
486 			foreach(vi, v ; t.vertices)
487 				enforce!TRRNInvalidValueException(v < vtxLen,
488 					format!"triangles[%d].vertices[%d] = %d is out of bounds [0;%d["(ti, vi, v, vtxLen));
489 		}
490 	}
491 }
492 
493 /// Water information (WATR)
494 struct TrnNWN2WaterPayload{
495 	/// WATR name.
496 	///
497 	/// NWN2 seems to set it to `""` and fill the remaining bytes with garbage.
498 	char[32] name;
499 	ubyte[96] unknown;///
500 	float[3] color;/// R,G,B
501 	float[2] ripple;/// Ripples
502 	float smoothness;/// Smoothness
503 	float reflect_bias;/// Reflection bias
504 	float reflect_power;/// Reflection power
505 	float specular_power;/// Specular map power
506 	float specular_cofficient;/// Specular map coefficient
507 	///
508 	static align(1) struct Texture{
509 		static assert(this.sizeof == 48);
510 		char[32] name;/// Texture name
511 		float[2] direction;/// Scrolling direction
512 		float rate;/// Scrolling speed
513 		float angle;/// Scrolling angle in radiant
514 	}
515 	Texture[3] textures;/// Water textures
516 	float[2] uv_offset;/// x,y offset in water-space <=> megatile_coordinates/8
517 	///
518 	static align(1) struct Vertex{
519 		static assert(this.sizeof == 28);
520 		float[3] position;/// x y z
521 		float[2] uvx5;/// XY5 (set to XY1 * 5.0)
522 		float[2] uv;/// XY1
523 	}
524 	///
525 	Vertex[] vertices;
526 	///
527 	static align(1) struct Triangle{
528 		static assert(this.sizeof == 6);
529 		uint16_t[3] vertices;///Triangle vertex indices in $(D TrnNWN2WaterPayload.vertices)
530 	}
531 	/// Walkmesh grid triangles positions.
532 	/// Each uint16_t an index in `vertices` corresponding to a triangle vertex
533 	Triangle[] triangles;
534 	uint32_t[] triangles_flags;/// 0 = has water, 1 = no water
535 	ubyte[] dds;/// DDS bitmap
536 	uint32_t[2] megatile_position;/// Position of the associated megatile in the terrain
537 
538 
539 	/// Build packet with raw data
540 	this(in ubyte[] payload){
541 		auto data = ChunkReader(payload);
542 
543 		name                = data.read!(typeof(name));
544 		unknown             = data.read!(typeof(unknown));
545 		color               = data.read!(typeof(color));
546 		ripple              = data.read!(typeof(ripple));
547 		smoothness          = data.read!(typeof(smoothness));
548 		reflect_bias        = data.read!(typeof(reflect_bias));
549 		reflect_power       = data.read!(typeof(reflect_power));
550 		specular_power      = data.read!(typeof(specular_power));
551 		specular_cofficient = data.read!(typeof(specular_cofficient));
552 		textures            = data.read!(typeof(textures));
553 		uv_offset           = data.read!(typeof(uv_offset));
554 
555 		immutable vertices_length  = data.read!uint32_t;
556 		immutable triangles_length = data.read!uint32_t;
557 		assert(data.read_ptr == 328);
558 
559 		vertices = data.readArray!Vertex(vertices_length).dup;
560 		triangles = data.readArray!Triangle(triangles_length).dup;
561 		triangles_flags = data.readArray!uint32_t(triangles_length).dup;
562 
563 		immutable dds_length = data.read!uint32_t;
564 		dds = data.readArray(dds_length).dup;
565 
566 		megatile_position = data.read!(typeof(megatile_position));
567 
568 		enforce!TrnParseException(data.read_ptr == payload.length,
569 			(payload.length - data.read_ptr).to!string ~ " bytes were not read at the end of WATR");
570 
571 		validate();
572 	}
573 
574 	///
575 	ubyte[] serialize() const {
576 		ChunkWriter cw;
577 		cw.put(
578 			name,
579 			unknown,
580 			color,
581 		);
582 		// arguments are separated because of https://issues.dlang.org/show_bug.cgi?id=21301
583 		cw.put(
584 			ripple,
585 			smoothness,
586 			reflect_bias,
587 			reflect_power,
588 			specular_power,
589 			specular_cofficient,
590 			textures,
591 			uv_offset,
592 			vertices.length.to!uint32_t,
593 			triangles.length.to!uint32_t,
594 			vertices,
595 			triangles,
596 			triangles_flags,
597 			dds.length.to!uint32_t, dds,
598 			megatile_position,
599 		);
600 		return cw.data;
601 	}
602 
603 	///
604 	void validate(bool strict = false) const {
605 		import nwn.dds;
606 
607 		// TODO: can't parse this kind of DDS atm
608 		//try new Dds(dds);
609 		//catch(Exception e)
610 		//	throw new WATRInvalidValueException("dds is invalid or format is not supported", __FILE__, __LINE__, e);
611 
612 		enforce!WATRInvalidValueException(triangles.length == triangles_flags.length,
613 			format!"triangles.length (=%d) must match triangles_flags.length (=%d)"(triangles.length, triangles_flags.length));
614 
615 		foreach(vi, v ; vertices){
616 			enforce!WATRInvalidValueException(v.position[].all!"!a.isNaN",
617 				format!"vertices[%d] has an invalid position: %s"(vi, v));
618 		}
619 
620 		immutable vtxLen = vertices.length;
621 		foreach(ti, ref t ; triangles){
622 			foreach(vi, v ; t.vertices)
623 				enforce!WATRInvalidValueException(v < vtxLen,
624 					format!"triangles[%d].vertices[%d] = %d is out of bounds [0;%d["(ti, vi, v, vtxLen));
625 		}
626 
627 		if(strict){
628 			foreach(i, f ; triangles_flags){
629 				enforce!WATRInvalidValueException(f == 0 || f == 1,
630 					format!"triangles_flags[%d]: Unknown flag value: %b"(i, f));
631 			}
632 		}
633 	}
634 
635 
636 	string dump() const {
637 		string ret;
638 		ret ~= format!"name: %s\n"(name);
639 		ret ~= format!"unknown: %(%02x %)\n"(unknown);
640 		ret ~= format!"color: %s\n"(color);
641 		ret ~= format!"ripple: %s\n"(ripple);
642 		ret ~= format!"smoothness: %s\n"(smoothness);
643 		ret ~= format!"reflect_bias: %s\n"(reflect_bias);
644 		ret ~= format!"reflect_power: %s\n"(reflect_power);
645 		ret ~= format!"specular_power: %s\n"(specular_power);
646 		ret ~= format!"specular_cofficient: %s\n"(specular_cofficient);
647 
648 		foreach(i, tex ; textures){
649 			ret ~= format!"texture.%d:\n"(i);
650 			ret ~= format!"    name: %s:\n"(tex.name);
651 			ret ~= format!"    direction: %s:\n"(tex.direction);
652 			ret ~= format!"    rate: %s:\n"(tex.rate);
653 			ret ~= format!"    angle: %s:\n"(tex.angle);
654 		}
655 
656 		ret ~= format!"uv_offset: %s\n"(uv_offset);
657 
658 		ret ~= "vertices:\n";
659 		foreach(i, vtx ; vertices)
660 			ret ~= format!"    %d: %s\n"(i, vtx);
661 
662 		ret ~= "triangles:\n";
663 		foreach(i, tri ; triangles)
664 			ret ~= format!"    %d: %s\n"(i, tri);
665 
666 		ret ~= "triangles_flags:\n";
667 		foreach(i, tf ; triangles_flags)
668 			ret ~= format!"    %d: %s\n"(i, tf);
669 
670 		ret ~= "dds:\n";
671 		ret ~= dds.dumpByteArray;
672 
673 		ret ~= format!"megatile_position: %s\n"(megatile_position);
674 
675 		return ret;
676 	}
677 }
678 
679 /// Compressed walkmesh (only contained inside TRX files) (ASWM)
680 struct TrnNWN2WalkmeshPayload{
681 
682 	/// ASWM packet header
683 	static align(1) struct Header{
684 		static assert(this.sizeof == 53);
685 		align(1):
686 		/// ASWM version. NWN2Toolset generates version 0x6c, but some NWN2 official campaign files are between 0x69 and 0x6c.
687 		uint32_t aswm_version;
688 		/// ASWM name (probably useless)
689 		char[32] name;
690 		/// Always true
691 		bool owns_data;
692 		private uint32_t vertices_count;
693 		private uint32_t edges_count;
694 		private uint32_t triangles_count;
695 		uint32_t unknownB;
696 	}
697 	/// ditto
698 	Header header;
699 
700 	static align(1) union Vertex {
701 		static assert(this.sizeof == 12);
702 		align(1):
703 
704 		float[3] position;
705 
706 		private static struct Xyz{ float x, y, z; }
707 		Xyz _xyz;
708 		alias _xyz this;
709 	}
710 	Vertex[] vertices;
711 
712 	/// Edge between two triangles
713 	static align(1) struct Edge{
714 		static assert(this.sizeof == 16);
715 		align(1):
716 		uint32_t[2] vertices; /// Vertex indices drawing the edge line
717 		uint32_t[2] triangles; /// Joined triangles (`uint32_t.max` if none)
718 	}
719 	/// For v69 only
720 	private static align(1) struct Edge_v69 {
721 		static assert(this.sizeof == 52);
722 		align(1):
723 		uint32_t[2] vertices; /// Vertex indices drawing the edge line
724 		uint32_t[2] triangles; /// Joined triangles (`uint32_t.max` if none)
725 		uint32_t[9] _reserved;
726 		Edge upgrade() const{
727 			return Edge(vertices, triangles);
728 		}
729 	}
730 	Edge[] edges;
731 
732 	/// Mesh Triangle + pre-calculated data + metadata
733 	static align(1) struct Triangle{
734 		static assert(this.sizeof == 64);
735 		align(1):
736 		uint32_t[3] vertices; /// Vertex indices composing the triangle
737 		/// Edges to other triangles (`uint32_t.max` if none, but there should always be 3)
738 		///
739 		/// Every `linked_edges` should have its associated `linked_triangles` at the same index
740 		uint32_t[3] linked_edges;
741 		/// Adjacent triangles (`uint32_t.max` if none)
742 		///
743 		/// Every `linked_triangles` should have its associated `linked_edges` at the same index
744 		uint32_t[3] linked_triangles;
745 		float[2] center; /// X / Y coordinates of the center of the triangle. Calculated by avg the 3 vertices coordinates.
746 		float[3] normal; /// Normal vector
747 		float dot_product; /// Dot product at plane
748 		uint16_t island; /// Index in the `TrnNWN2WalkmeshPayload.islands` array.
749 		uint16_t flags; /// See `Flags`
750 
751 		enum Flags {
752 			walkable  = 0x01, /// if the triangle can be walked on. Note the triangle needs path tables to be really walkable
753 			clockwise = 0x04, /// vertices are wound clockwise and not ccw
754 			dirt      = 0x08, /// Floor type (for sound effects)
755 			grass     = 0x10, /// ditto
756 			stone     = 0x20, /// ditto
757 			wood      = 0x40, /// ditto
758 			carpet    = 0x80, /// ditto
759 			metal     = 0x100, /// ditto
760 			swamp     = 0x200, /// ditto
761 			mud       = 0x400, /// ditto
762 			leaves    = 0x800, /// ditto
763 			water     = 0x1000, /// ditto
764 			puddles   = 0x2000, /// ditto
765 
766 			soundstepFlags = dirt | grass | stone | wood | carpet | metal | swamp | mud | leaves | water | puddles,
767 		}
768 	}
769 	/// For v6a only
770 	private static align(1) struct Triangle_v6a{
771 		static assert(this.sizeof == 68);
772 		align(1):
773 		uint32_t  _reserved;
774 		uint32_t[3] vertices;
775 		uint32_t[3] linked_edges;
776 		uint32_t[3] linked_triangles;
777 		float[2] center;
778 		float[3] normal;
779 		float dot_product;
780 		uint16_t island;
781 		uint16_t flags;
782 		Triangle upgrade() const{
783 			return Triangle(vertices, linked_edges, linked_triangles, center, normal, dot_product, island, flags);
784 		}
785 	}
786 	/// For v69 only
787 	private static align(1) struct Triangle_v69{
788 		static assert(this.sizeof == 88);
789 		align(1):
790 		uint32_t  _reserved0;
791 		uint32_t flags;
792 		uint32_t[3] vertices;
793 		uint32_t[3] linked_edges;
794 		uint32_t[3] linked_triangles;
795 		uint32_t[3]  _reserved1;
796 		float[2] center;
797 		uint32_t  _reserved2;
798 		float[3] normal;
799 		float dot_product;
800 		uint16_t  _reserved3;
801 		uint16_t island;
802 		Triangle upgrade() const{
803 			return Triangle(vertices, linked_edges, linked_triangles, center, normal, dot_product, island, cast(uint16_t)flags);
804 		}
805 	}
806 	Triangle[] triangles;
807 
808 	/// Always 31 in TRX files, 15 in TRN files
809 	uint32_t tiles_flags;
810 	/// Width in meters of a terrain tile (most likely to be 10.0)
811 	float tiles_width;
812 	/// Number of tiles along Y axis
813 	/// TODO: double check height = Y
814 	uint32_t tiles_grid_height;
815 	/// Number of tiles along X axis
816 	/// TODO: double check width = X
817 	uint32_t tiles_grid_width;
818 	/// Width of the map borders in tiles (8 means that 8 tiles will be removed on each side)
819 	uint32_t tiles_border_size;
820 
821 	/// Tile with its path table
822 	static struct Tile {
823 
824 		static align(1) struct Header {
825 			static assert(this.sizeof == 57);
826 			align(1):
827 			char[32] name; /// Last time I checked it was complete garbage
828 			ubyte owns_data;/// 1 if the tile stores vertices / edges. Usually 0
829 			uint32_t vertices_count; /// Number of vertices in this tile
830 			uint32_t edges_count; /// Number of edges in this tile
831 			uint32_t triangles_count; /// Number of triangles in this tile (walkable + unwalkable)
832 			float size_x;/// Always 0 ?
833 			float size_y;/// Always 0 ?
834 
835 			/// This value will be added to each triangle index in the PathTable
836 			uint32_t triangles_offset;
837 		}
838 		Header header;
839 
840 		/// Only used if `header.owns_data == true`
841 		Vertex[] vertices;
842 
843 		/// Only used if `header.owns_data == true`
844 		Edge[] edges;
845 
846 		/**
847 		Tile pathing information
848 
849 		Notes:
850 		- "local" refers to the local triangle index. The aswm triangle index
851 		  can be retrieved by adding Tile.triangles_offset
852 		- Each triangle referenced here is only referenced once across all the
853 		  tiles of the ASWM
854 		*/
855 		static struct PathTable {
856 
857 			static align(1) struct Header {
858 				static assert(this.sizeof == 13);
859 				align(1):
860 
861 				enum Flags {
862 					rle       = 0x01,
863 					zcompress = 0x02,
864 				}
865 				uint32_t flags; /// Always 0. Used to set path table compression
866 				private uint32_t _local_to_node_length; /// use `local_to_node.length` instead
867 				private ubyte _node_to_local_length; /// use `node_to_local.length` instead
868 				uint32_t rle_table_size; /// Always 0 ? probably related to Run-Length Encoding
869 			}
870 			/// For v69 to v6b
871 			private static align(1) struct Header_v69 {
872 				static assert(this.sizeof == 10);
873 				align(1):
874 				uint32_t flags;
875 				ubyte _local_to_node_length;
876 				ubyte _node_to_local_length;
877 				uint32_t rle_table_size;
878 				Header upgrade() const{
879 					return Header(flags, _local_to_node_length, _node_to_local_length, rle_table_size);
880 				}
881 			}
882 			Header header;
883 
884 			/**
885 			List of node indices for each triangle in the tile
886 
887 			`local_to_node[triangle_local_index]` represents an index value to
888 			be used with nodes (see `nodes` for how to use it)
889 
890 			All triangles (even non walkable) must be represented here.
891 			Triangle indices that are not used in this tile must have a `0xFF`
892 			value.
893 			*/
894 			ubyte[] local_to_node;
895 
896 			/**
897 			Node index to local triangle index
898 
899 			Values must not be uint32_t.max
900 			*/
901 			uint32_t[] node_to_local;
902 
903 			/**
904 			Node list
905 
906 			This is used to determine which triangle a creature should go next
907 			to reach a destination triangle.
908 
909 			`nodes[header.node_to_local_length * FromLTNIndex + DestLTNIndex]
910 			& 0b0111_1111` is an index in `node_to_local` array, containing
911 			the next triangle to go to in order to reach destination.
912 
913 			`FromLTNIndex`, `DestLTNIndex` are values found inside the
914 			`local_to_node` array.
915 			<ul>
916 			$(LI `value & 0b0111_1111` is an index in `node_to_local` table)
917 			$(LI `value & 0b1000_0000` is > 0 if there is a clear line of
918 			sight between the two triangle. It's not clear what LOS is since
919 			two linked triangles on flat ground may not have LOS = 1 in game
920 			files.)
921 			</ul>
922 
923 			If FromLTNIndex == DestLTNIndex, the value must be set to 255.
924 
925 			Note: does not contain any 127 = 0b0111_1111 values
926 			*/
927 			ubyte[] nodes;
928 
929 			/// Always 0b0001_1111 = 31 ?
930 			uint32_t flags;
931 		}
932 		PathTable path_table;
933 
934 		private void parse(ref ChunkReader wmdata, uint32_t aswmVersion=0x6c){
935 			header = wmdata.read!Header;
936 
937 			if(header.owns_data){
938 				vertices = wmdata.readArray!Vertex(header.vertices_count).dup;
939 				switch(aswmVersion){
940 					case 0x69:
941 						edges = wmdata.readArray!Edge_v69(header.edges_count).map!(a => a.upgrade()).array;
942 						break;
943 					case 0x6a: .. case 0x6c:
944 						edges = wmdata.readArray!Edge(header.edges_count).dup;
945 						break;
946 					default: enforce(0, "Unsupported ASWM version " ~ aswmVersion.format!"0x%02x");
947 				}
948 			}
949 
950 			with(path_table){
951 				switch(aswmVersion){
952 					case 0x69: .. case 0x6b:
953 						header = wmdata.read!Header_v69.upgrade();
954 						break;
955 					case 0x6c:
956 						header = wmdata.read!Header;
957 						break;
958 					default: enforce(0, "Unsupported ASWM version " ~ aswmVersion.format!"0x%02x");
959 				}
960 
961 				enforce!TrnParseException((header.flags & (Header.Flags.rle | Header.Flags.zcompress)) == 0, "Compressed path tables not supported");
962 
963 				local_to_node = wmdata.readArray!ubyte(header._local_to_node_length).dup;
964 				switch(aswmVersion){
965 					case 0x69, 0x6a:
966 						// No node_to_local data
967 						break;
968 					case 0x6b:
969 						node_to_local = wmdata.readArray!uint8_t(header._node_to_local_length).map!(a => cast(uint32_t)a).array;
970 						break;
971 					case 0x6c:
972 						node_to_local = wmdata.readArray!uint32_t(header._node_to_local_length).dup;
973 						break;
974 					default: enforce(0, "Unsupported ASWM version " ~ aswmVersion.format!"0x%02x");
975 				}
976 				nodes = wmdata.readArray!ubyte(header._node_to_local_length ^^ 2).dup;
977 
978 				flags = wmdata.read!(typeof(flags));
979 			}
980 		}
981 		private void serialize(ref ChunkWriter uncompData) const {
982 
983 			uncompData.put(
984 				header,
985 				vertices,
986 				edges);
987 
988 			immutable tcount = header.triangles_count;
989 
990 			with(path_table){
991 				// Update header
992 				PathTable.Header updatedHeader = header;
993 				updatedHeader._local_to_node_length = local_to_node.length.to!uint32_t;
994 				updatedHeader._node_to_local_length = node_to_local.length.to!ubyte;
995 
996 				assert(nodes.length == node_to_local.length ^^ 2, "Bad number of path table nodes");
997 				assert(local_to_node.length == tcount, "local_to_node length should match header.triangles_count");
998 
999 				// serialize
1000 				uncompData.put(
1001 					updatedHeader,
1002 					local_to_node,
1003 					node_to_local,
1004 					nodes,
1005 					flags);
1006 			}
1007 		}
1008 
1009 		string dump() const {
1010 			import std.range: chunks;
1011 			return format!"TILE header: name: %(%s, %)\n"([header.name])
1012 			     ~ format!"        owns_data: %s, vert_cnt: %s, edge_cnt: %s, tri_cnt: %s\n"(header.owns_data, header.vertices_count, header.edges_count, header.triangles_count)
1013 			     ~ format!"        size_x: %s, size_y: %s\n"(header.size_x, header.size_y)
1014 			     ~ format!"        triangles_offset: %s\n"(header.triangles_offset)
1015 			     ~ format!"     vertices: %s\n"(vertices)
1016 			     ~ format!"     edges: %s\n"(edges)
1017 			     ~        "     path_table: \n"
1018 			     ~ format!"       header: flags: %s, ltn_len: %d, ntl_len: %s, rle_len: %s\n"(path_table.header.flags, path_table.header._local_to_node_length, path_table.header._node_to_local_length, path_table.header.rle_table_size)
1019 			     ~ format!"       ltn:   %(%3d %)\n"(path_table.local_to_node)
1020 			     ~ format!"       ntl:   %(%3d %)\n"(path_table.node_to_local)
1021 			     ~ format!"       nodes: %(%-(%s%)\n              %)\n"(
1022 			     	path_table.node_to_local.length == 0 ?
1023 			     	[] : path_table.nodes.map!(a => (((a & 128)? "*" : " ") ~ (a & 127).to!string).rightJustify(4)).chunks(path_table.node_to_local.length).array)
1024 			     ~ format!"       flags: %s\n"(path_table.flags);
1025 		}
1026 
1027 		ubyte getPathNode(uint32_t fromGTriIndex, uint32_t toGTriIndex) const {
1028 			assert(header.triangles_offset <= fromGTriIndex && fromGTriIndex < path_table.local_to_node.length + header.triangles_offset,
1029 				"From triangle index "~fromGTriIndex.to!string~" is not in tile path table");
1030 			assert(header.triangles_offset <= toGTriIndex && toGTriIndex < path_table.local_to_node.length + header.triangles_offset,
1031 				"To triangle index "~toGTriIndex.to!string~" is not in tile path table");
1032 
1033 
1034 			immutable nodeFrom = path_table.local_to_node[fromGTriIndex - header.triangles_offset];
1035 			immutable nodeTo = path_table.local_to_node[toGTriIndex - header.triangles_offset];
1036 
1037 			if(nodeFrom == 0xff || nodeTo == 0xff)
1038 				return 0xff;
1039 
1040 			return path_table.nodes[nodeFrom * path_table.node_to_local.length + nodeTo];
1041 		}
1042 
1043 		/**
1044 		Calculate the fastest route between two triangles of a tile. The tile need to be baked, as it uses existing path tables.
1045 		*/
1046 		uint32_t[] findPath(in uint32_t fromGTriIndex, in uint32_t toGTriIndex) const {
1047 			assert(header.triangles_offset <= fromGTriIndex && fromGTriIndex < path_table.local_to_node.length + header.triangles_offset,
1048 				"From triangle index "~fromGTriIndex.to!string~" is not in tile path table");
1049 			assert(header.triangles_offset <= toGTriIndex && toGTriIndex < path_table.local_to_node.length + header.triangles_offset,
1050 				"To triangle index "~toGTriIndex.to!string~" is not in tile path table");
1051 
1052 			uint32_t from = fromGTriIndex;
1053 
1054 			int iSec = 0;
1055 			uint32_t[] ret;
1056 			while(from != toGTriIndex && iSec++ < 1000){
1057 				auto node = getPathNode(from, toGTriIndex);
1058 				if(node == 0xff)
1059 					return ret;
1060 
1061 				from = path_table.node_to_local[node & 0b0111_1111] + header.triangles_offset;
1062 				ret ~= from;
1063 
1064 			}
1065 			assert(iSec < 1000, "Tile precalculated paths lead to a loop (from="~fromGTriIndex.to!string~", to="~toGTriIndex.to!string~")");
1066 			return ret;
1067 		}
1068 
1069 		/// Check a single tile. You should use `TrnNWN2WalkmeshPayload.validate()` instead
1070 		void validate(in TrnNWN2WalkmeshPayload aswm, uint32_t tileIndex, bool strict = false) const {
1071 			import std.typecons: Tuple;
1072 			alias Ret = Tuple!(bool,"valid", string,"error");
1073 			immutable nodesLen = path_table.nodes.length;
1074 			immutable ntlLen = path_table.node_to_local.length;
1075 			immutable ltnLen = path_table.local_to_node.length;
1076 			immutable offset = header.triangles_offset;
1077 
1078 			enforce!ASWMInvalidValueException(ltnLen == 0 || header.triangles_count == ltnLen,
1079 				"local_to_node: length ("~ltnLen.to!string~") does not match triangles_count ("~header.triangles_count.to!string~")");
1080 
1081 			enforce!ASWMInvalidValueException(offset < aswm.triangles.length || (offset == aswm.triangles.length && ltnLen == 0),
1082 				"header.triangles_offset: offset ("~offset.to!string~") points to invalid triangles");
1083 
1084 			enforce!ASWMInvalidValueException(offset + ltnLen <= aswm.triangles.length,
1085 				"local_to_node: contains data for invalid triangles");
1086 
1087 
1088 			if(strict){
1089 				immutable edgeCnt = aswm.triangles[offset .. offset + header.triangles_count]
1090 					.map!((ref a) => a.linked_edges[])
1091 					.join
1092 					.filter!(a => a != a.max)
1093 					.array.dup
1094 					.sort
1095 					.uniq
1096 					.array.length.to!uint32_t;
1097 				immutable vertCnt = aswm.triangles[offset .. offset + header.triangles_count]
1098 					.map!((ref a) => a.vertices[])
1099 					.join
1100 					.filter!(a => a != a.max)
1101 					.array.dup
1102 					.sort
1103 					.uniq
1104 					.array.length.to!uint32_t;
1105 
1106 				enforce!ASWMInvalidValueException(edgeCnt == header.edges_count,
1107 					"header.edges_count: Wrong number of edges: got "~header.edges_count.to!string~", counted "~edgeCnt.to!string);
1108 				enforce!ASWMInvalidValueException(vertCnt == header.vertices_count,
1109 					"header.vertices_count: Wrong number of vertices: got "~header.vertices_count.to!string~", counted "~vertCnt.to!string);
1110 			}
1111 
1112 			if(strict){
1113 				uint32_t tileX = tileIndex % aswm.tiles_grid_width;
1114 				uint32_t tileY = tileIndex / aswm.tiles_grid_width;
1115 				auto tileAABB = box2f(
1116 					vec2f(tileX * aswm.tiles_width,       tileY * aswm.tiles_width),
1117 					vec2f((tileX + 1) * aswm.tiles_width, (tileY + 1) * aswm.tiles_width));
1118 
1119 				foreach(i ; offset .. offset + header.triangles_count){
1120 					enforce!ASWMInvalidValueException(tileAABB.contains(vec2f(aswm.triangles[i].center)),
1121 						"Triangle "~i.to!string~" is outside of the tile AABB");
1122 				}
1123 			}
1124 
1125 			// Path table
1126 			if(aswm.header.aswm_version >= 0x6b){
1127 				enforce!ASWMInvalidValueException(nodesLen == ntlLen ^^ 2,
1128 					format!"Wrong number of nodes (%d instead of %d)"(nodesLen, ntlLen ^^ 2));
1129 			}
1130 			else{
1131 				enforce!ASWMInvalidValueException(ntlLen == 0,
1132 					"Wrong number of nodes (shoule be 0)");
1133 			}
1134 
1135 			if(nodesLen < 0x7f){
1136 				foreach(j, node ; path_table.nodes){
1137 					enforce!ASWMInvalidValueException(node == 0xff || (node & 0b0111_1111) < ntlLen,
1138 						"nodes["~j.to!string~"]: Illegal value "~node.to!string ~ " (should be either 255 or less than 127)");
1139 				}
1140 			}
1141 			if(nodesLen < 0xff){
1142 				foreach(j, node ; path_table.local_to_node){
1143 					enforce!ASWMInvalidValueException(node == 0xff || node < nodesLen,
1144 						"local_to_node["~j.to!string~"]: Illegal value"~node.to!string ~ " (should be either 255 or an existing node index)");
1145 				}
1146 			}
1147 
1148 			foreach(j, ntl ; path_table.node_to_local){
1149 				enforce!ASWMInvalidValueException(ntl + offset < aswm.triangles.length,
1150 					"node_to_local["~j.to!string~"]: triangle index "~ntl.to!string~" out of bounds");
1151 			}
1152 		}
1153 	}
1154 	/// Map tile list
1155 	/// Non border tiles have `header.vertices_count > 0 || header.edges_count > 0 || header.triangles_count > 0`
1156 	Tile[] tiles;
1157 
1158 	/**
1159 	Tile or fraction of a tile used for pathfinding through large distances.
1160 
1161 	<ul>
1162 	<li>The island boundaries match exactly the tile boundaries</li>
1163 	<li>Generally you have one island per tile.</li>
1164 	<li>You can have multiple islands for one tile, like if one side of the tile is not accessible from the other side</li>
1165 	</ul>
1166 	*/
1167 	static struct Island {
1168 		static align(1) struct Header {
1169 			static assert(this.sizeof == 24);
1170 			align(1):
1171 			uint32_t index; /// Index of the island in the aswm.islands array. TODO: weird
1172 			uint32_t tile; /// Value looks pretty random, but is identical for all islands
1173 			Vertex center; /// Center of the island. Z is always 0. TODO: find how it is calculated
1174 			uint32_t triangles_count; /// Number of triangles in this island
1175 		}
1176 		Header header;
1177 		uint32_t[] adjacent_islands; /// Adjacent islands
1178 		float[] adjacent_islands_dist; /// Distances between adjacent islands (probably measured between header.center)
1179 
1180 		/**
1181 		List of triangles that are on the island borders, and which linked_edges
1182 		can lead to a triangle that have another triangle.island value.
1183 
1184 		<ul>
1185 		<li>There is no need to register all possible exit triangles. Only one per adjacent island is enough.</li>
1186 		<li>Generally it is 4 walkable triangles: 1 top left, 2 bot left and 1 bot right</li>
1187 		</ul>
1188 		*/
1189 		uint32_t[] exit_triangles;
1190 
1191 		private void parse(ref ChunkReader wmdata){
1192 			header = wmdata.read!(typeof(header));
1193 
1194 			immutable adjLen = wmdata.read!uint32_t;
1195 			adjacent_islands = wmdata.readArray!uint32_t(adjLen).dup;
1196 
1197 			immutable adjDistLen = wmdata.read!uint32_t;
1198 			adjacent_islands_dist = wmdata.readArray!float(adjDistLen).dup;
1199 
1200 			immutable exitLen = wmdata.read!uint32_t;
1201 			exit_triangles = wmdata.readArray!uint32_t(exitLen).dup;
1202 		}
1203 		private void serialize(ref ChunkWriter uncompData) const {
1204 			uncompData.put(
1205 				header,
1206 				cast(uint32_t)adjacent_islands.length,
1207 				adjacent_islands,
1208 				cast(uint32_t)adjacent_islands_dist.length,
1209 				adjacent_islands_dist,
1210 				cast(uint32_t)exit_triangles.length,
1211 				exit_triangles);
1212 		}
1213 
1214 		string dump() const {
1215 			return format!"ISLA header: index: %s, tile: %s, center: %s, triangles_count: %s\n"(header.index, header.tile, header.center.position, header.triangles_count)
1216 			     ~ format!"      adjacent_islands: %s\n"(adjacent_islands)
1217 			     ~ format!"      adjacent_islands_dist: %s\n"(adjacent_islands_dist)
1218 			     ~ format!"      exit_triangles: %s\n"(exit_triangles);
1219 		}
1220 	}
1221 
1222 	/// Islands list. See `Island`
1223 	Island[] islands;
1224 
1225 
1226 	static align(1) struct IslandPathNode {
1227 		static assert(this.sizeof == 8);
1228 		uint16_t next; /// Next island index to go to
1229 		uint16_t _padding;
1230 		float weight; /// Distance to `next` island.
1231 	}
1232 	IslandPathNode[] islands_path_nodes;
1233 
1234 
1235 	/// Build packet with raw data
1236 	this(in ubyte[] payload){
1237 		auto data = ChunkReader(payload);
1238 
1239 		ChunkReader* wmdata;
1240 
1241 		auto comp_type = data.read!(char[4]);
1242 		if(comp_type == "COMP"){
1243 			immutable comp_length   = data.read!uint32_t;
1244 			immutable uncomp_length = data.read!uint32_t;
1245 
1246 			auto comp_wm = data.readArray(comp_length);
1247 
1248 			// zlib deflate
1249 			import std.zlib: uncompress;
1250 			auto walkmeshData = cast(ubyte[])uncompress(comp_wm, uncomp_length);
1251 			assert(walkmeshData.length == uncomp_length, "Length mismatch");
1252 
1253 			wmdata = new ChunkReader(walkmeshData);
1254 		}
1255 		else{
1256 			wmdata = new ChunkReader(payload);
1257 		}
1258 
1259 		header = wmdata.read!Header;
1260 		assert(wmdata.read_ptr==0x35);
1261 		enforce!TrnParseException(header.owns_data, "ASWM packet does not own any data (`header.owns_data=false`)");
1262 
1263 		vertices       = wmdata.readArray!Vertex(header.vertices_count).dup;
1264 		switch(header.aswm_version){
1265 			case 0x69:
1266 				edges = wmdata.readArray!Edge_v69(header.edges_count).map!(a => a.upgrade()).array;
1267 				break;
1268 			case 0x6a: .. case 0x6c:
1269 				edges = wmdata.readArray!Edge(header.edges_count).dup;
1270 				break;
1271 			default: enforce(0, "Unsupported ASWM version " ~ header.aswm_version.format!"0x%02x");
1272 		}
1273 		switch(header.aswm_version){
1274 			case 0x69:
1275 				triangles = wmdata.readArray!Triangle_v69(header.triangles_count).map!(a => a.upgrade()).array;
1276 				break;
1277 			case 0x6a:
1278 				triangles = wmdata.readArray!Triangle_v6a(header.triangles_count).map!(a => a.upgrade()).array;
1279 				break;
1280 			case 0x6b, 0x6c:
1281 				triangles = wmdata.readArray!Triangle(header.triangles_count).dup;
1282 				break;
1283 			default: enforce(0, "Unsupported ASWM version " ~ header.aswm_version.format!"0x%02x");
1284 		}
1285 
1286 		tiles_flags       = wmdata.read!(typeof(tiles_flags));
1287 		tiles_width       = wmdata.read!(typeof(tiles_width));
1288 		tiles_grid_height = wmdata.read!(typeof(tiles_grid_height));
1289 		tiles_grid_width  = wmdata.read!(typeof(tiles_grid_width));
1290 
1291 		// Tile list
1292 		tiles.length = tiles_grid_height * tiles_grid_width;
1293 		foreach(i, ref tile ; tiles){
1294 			// Path table
1295 			tile.parse(*wmdata, header.aswm_version);
1296 		}
1297 
1298 		tiles_border_size = wmdata.read!(typeof(tiles_border_size));
1299 
1300 		// Islands list
1301 		islands.length = wmdata.read!uint32_t;
1302 		foreach(ref island ; islands){
1303 			island.parse(*wmdata);
1304 		}
1305 
1306 		islands_path_nodes = wmdata.readArray!IslandPathNode(islands.length ^^ 2).dup;
1307 
1308 		enforce!TrnParseException(wmdata.read_ptr == wmdata.data.length,
1309 			(wmdata.data.length - wmdata.read_ptr).to!string ~ " bytes were not read at the end of ASWM");
1310 
1311 		version(unittest){
1312 			auto serialized = serializeUncompressed();
1313 			assert(serialized.length == wmdata.data.length, "mismatch length "~wmdata.data.length.to!string~" -> "~serialized.length.to!string);
1314 			assert(wmdata.data == serialized, "Could not serialize correctly");
1315 		}
1316 	}
1317 
1318 
1319 	/**
1320 	Serialize TRN packet data
1321 	*/
1322 	ubyte[] serialize() const {
1323 		auto uncompData = serializeUncompressed();
1324 
1325 		import std.zlib: compress;
1326 		const compData = compress(uncompData);
1327 
1328 		const compLength = compData.length.to!uint32_t;
1329 		const uncompLength = uncompData.length.to!uint32_t;
1330 
1331 
1332 		ChunkWriter cw;
1333 		cw.put(cast(char[4])"COMP", compLength, uncompLength, compData);
1334 		return cw.data;
1335 	}
1336 
1337 	/**
1338 	Serialize the aswm data without compressing it. Useful for debugging raw data.
1339 	*/
1340 	ubyte[] serializeUncompressed() const {
1341 		//update header values
1342 		Header updatedHeader = header;
1343 		updatedHeader.aswm_version = 0x6c; // Only 0x6c serialization is supported
1344 		updatedHeader.owns_data = true;
1345 		updatedHeader.vertices_count  = vertices.length.to!uint32_t;
1346 		updatedHeader.edges_count = edges.length.to!uint32_t;
1347 		updatedHeader.triangles_count = triangles.length.to!uint32_t;
1348 
1349 		//build uncompressed data
1350 		ChunkWriter uncompData;
1351 		uncompData.put(
1352 			updatedHeader,
1353 			vertices,
1354 			edges,
1355 			triangles,
1356 			tiles_flags,
1357 			tiles_width,
1358 			tiles_grid_height,
1359 			tiles_grid_width);
1360 
1361 		foreach(ref tile ; tiles){
1362 			tile.serialize(uncompData);
1363 		}
1364 
1365 		uncompData.put(
1366 			tiles_border_size,
1367 			cast(uint32_t)islands.length);
1368 
1369 		foreach(ref island ; islands){
1370 			island.serialize(uncompData);
1371 		}
1372 
1373 		uncompData.put(islands_path_nodes);
1374 
1375 		return uncompData.data;
1376 	}
1377 
1378 	/**
1379 	Check if the ASWM contains legit data
1380 
1381 	Throws: ASWMInvalidValueException containing the error message
1382 	Args:
1383 	strict = false to allow some data inconsistencies that does not cause issues with nwn2
1384 	*/
1385 	void validate(bool strict = false) const {
1386 
1387 		immutable vertLen = vertices.length;
1388 		immutable edgeLen = edges.length;
1389 		immutable triLen = triangles.length;
1390 		immutable islLen = islands.length;
1391 
1392 		// Vertices
1393 		foreach(vi, v ; vertices){
1394 			enforce!ASWMInvalidValueException(v.position[].all!"!a.isNaN",
1395 				format!"vertices[%d] has an invalid position: %s"(vi, v));
1396 		}
1397 
1398 		// Edges
1399 		foreach(iedge, ref edge ; edges){
1400 			foreach(v ; edge.vertices){
1401 				enforce!ASWMInvalidValueException(v < vertLen,
1402 					format!"edges[%d]: Vertex index %d is out of range (0..%d)"(iedge, v, vertLen));
1403 			}
1404 			foreach(t ; edge.triangles){
1405 				enforce!ASWMInvalidValueException(t == uint32_t.max || t < triLen,
1406 					format!"edges[%d]: Triangle index %d is out of range (0..%d)"(iedge, t, triLen));
1407 			}
1408 		}
1409 
1410 		// Triangles
1411 		foreach(itri, ref tri ; triangles){
1412 			foreach(v ; tri.vertices){
1413 				enforce!ASWMInvalidValueException(v < vertLen,
1414 					format!"triangles[%d]: Vertex index %s is out of range (0..%d)"(itri, v, vertLen));
1415 			}
1416 
1417 			foreach(i ; 0 .. 3){
1418 				immutable lj = tri.linked_edges[i];
1419 				immutable lt = tri.linked_triangles[i];
1420 
1421 				enforce!ASWMInvalidValueException(lj < edgeLen,
1422 					format!"triangles[%d].linked_edges[%d]: Edge index %d is out of range (0..%d)"(itri, i, lj, edgeLen));
1423 
1424 				enforce!ASWMInvalidValueException(lt == uint32_t.max || lt < triLen,
1425 					format!"triangles[%d].linked_triangles[%d]: Triangle index %d is out of range (0..%d)"(itri, i, lt, triLen));
1426 
1427 				enforce!ASWMInvalidValueException(
1428 					(edges[lj].triangles[0] == itri && edges[lj].triangles[1] == lt)
1429 					|| (edges[lj].triangles[0] == lt && edges[lj].triangles[1] == itri),
1430 					format!"triangles[%d].linked_xxx[%d]: linked edge does not match linked triangle"(itri, i));
1431 			}
1432 
1433 			if(islLen == 0){
1434 				if(strict)
1435 					enforce!ASWMInvalidValueException(tri.island == uint16_t.max,
1436 						format!"triangles[%d].island: No islands are defined in the ASWM data. The island index %d should be 0xffff"(itri, tri.island));
1437 			}
1438 			else
1439 				enforce!ASWMInvalidValueException(tri.island == uint16_t.max || tri.island < islLen,
1440 					format!"triangles[%d].island: Island index %d is out of range (0..%d)"(itri, tri.island, islLen));
1441 		}
1442 
1443 		// Tiles
1444 		enforce!ASWMInvalidValueException(tiles.length == tiles_grid_width * tiles_grid_height,
1445 			format!"Wrong number of tiles: %d instead of %d (tiles_grid_width * tiles_grid_height)"(tiles.length, tiles_grid_width * tiles_grid_height));
1446 
1447 		enforce!ASWMInvalidValueException(tiles_width > 0.0,
1448 			"tiles_width: must be > 0");
1449 
1450 		foreach(i, ref tile ; tiles){
1451 			try tile.validate(this, cast(uint32_t)i, strict);
1452 			catch(ASWMInvalidValueException e){
1453 				e.msg = format!"tiles[%d]: %s"(i, e.msg);
1454 				throw e;
1455 			}
1456 		}
1457 
1458 		uint32_t[] overlapingTri;
1459 		overlapingTri.length = triangles.length;
1460 		overlapingTri[] = uint32_t.max;
1461 		foreach(i, ref tile ; tiles){
1462 			foreach(t ; tile.header.triangles_offset .. tile.header.triangles_offset + tile.header.triangles_count){
1463 				enforce!ASWMInvalidValueException(overlapingTri[t] == uint32_t.max,
1464 					format!"tiles[%d]: Triangle index %d (center=%s) is already owned by tile %d"(i, t, triangles[t].center, overlapingTri[t]));
1465 
1466 				overlapingTri[t] = cast(uint32_t)i;
1467 			}
1468 		}
1469 
1470 		// Islands
1471 		foreach(isli, ref island ; islands){
1472 
1473 			enforce!ASWMInvalidValueException(island.header.index == isli,
1474 				format!"islands[%d].header.index: does not match island index in islands array"(isli));
1475 
1476 			enforce!ASWMInvalidValueException(
1477 				island.adjacent_islands.length == island.adjacent_islands_dist.length
1478 				&& island.adjacent_islands.length == island.exit_triangles.length,
1479 				format!"islands[%d]: adjacent_islands/adjacent_islands_dist/exit_triangles lengths must match"(isli));
1480 
1481 			foreach(i ; 0 .. island.adjacent_islands.length){
1482 				enforce!ASWMInvalidValueException(island.adjacent_islands[i] < islLen,// Note: Skywing allows uint16_t.max value
1483 					format!"islands[%d].adjacent_islands[%d]: Island index %d is out of range (0..%d)"(isli, i, island.adjacent_islands[i], islLen));
1484 				enforce!ASWMInvalidValueException(island.exit_triangles[i] < triLen,
1485 					format!"islands[%d].exit_triangles[%d]: Triangle index %d is out of range (0..%d)"(isli, i, island.exit_triangles[i], triLen));
1486 
1487 
1488 				foreach(exitIdx, t ; island.exit_triangles){
1489 					enforce!ASWMInvalidValueException(triangles[t].island == isli,
1490 						format!"islands[%d].exit_triangles[%d]: Triangle index %d is not linked to this island"(isli, exitIdx, t));
1491 
1492 					bool found = false;
1493 					foreach(lt ; triangles[t].linked_triangles){
1494 						if(lt != uint32_t.max
1495 						&& triangles[lt].island == island.adjacent_islands[exitIdx]){
1496 							found = true;
1497 							break;
1498 						}
1499 					}
1500 					enforce!ASWMInvalidValueException(found,
1501 						format!"islands[%d].exit_triangles[%d]: Triangle index %d is not adjacent to island %d"(isli, exitIdx, t, island.adjacent_islands[exitIdx]));
1502 				}
1503 
1504 			}
1505 		}
1506 
1507 		// Island path nodes
1508 		enforce!ASWMInvalidValueException(islands_path_nodes.length == islands.length ^^ 2,
1509 			format!"Wrong number of islands / islands_path_nodes: %d instead of %d"(islands_path_nodes.length, islands.length ^^ 2));
1510 
1511 		foreach(i, ipn ; islands_path_nodes){
1512 			enforce!ASWMInvalidValueException(ipn.next == uint16_t.max || ipn.next < islLen,
1513 				format!"islands_path_nodes[%d]: Island index %d is out of range (0..%d)"(i, ipn.next, islLen));
1514 		}
1515 	}
1516 
1517 	/**
1518 	Dump trn data as text
1519 	*/
1520 	string dump() const {
1521 		import std.algorithm;
1522 		import std.array: array;
1523 
1524 		string ret;
1525 
1526 		ret ~= "==== HEADER ====\n";
1527 		ret ~= "aswm_version: " ~ header.aswm_version.to!string ~ "\n";
1528 		ret ~= "name: " ~ header.name.charArrayToString ~ "\n";
1529 		ret ~= "owns_data: " ~ header.owns_data.to!string ~ "\n";
1530 		ret ~= "vertices_count: " ~ header.vertices_count.to!string ~ "\n";
1531 		ret ~= "edges_count: " ~ header.edges_count.to!string ~ "\n";
1532 		ret ~= "triangles_count: " ~ header.triangles_count.to!string ~ "\n";
1533 		ret ~= "unknownB: " ~ header.unknownB.to!string ~ "\n";
1534 
1535 		ret ~= "==== VERTICES ====\n";
1536 		ret ~= vertices.map!(a => format!"VERT %s\n"(a.position)).join;
1537 
1538 		ret ~= "==== EDGES ====\n";
1539 		ret ~= edges.map!(a => format!"EDGE line: %s, tri: %s\n"(a.vertices, a.triangles)).join;
1540 
1541 		ret ~= "==== TRIANGLES ====\n";
1542 		ret ~= triangles.map!(a =>
1543 				  format!"TRI vert: %s, edge: %s, tri: %s\n"(a.vertices, a.linked_edges, a.linked_triangles)
1544 				~ format!"    center: %s, normal: %s, dot_product: %s\n"(a.center, a.normal, a.dot_product)
1545 				~ format!"    island: %s, flags: %s\n"(a.island, a.flags)
1546 			).join;
1547 
1548 		ret ~= "==== TILES HEADER ====\n";
1549 		ret ~= "tiles_flags: " ~ tiles_flags.to!string ~ "\n";
1550 		ret ~= "tiles_width: " ~ tiles_width.to!string ~ "\n";
1551 		ret ~= "tiles_grid_height: " ~ tiles_grid_height.to!string ~ "\n";
1552 		ret ~= "tiles_grid_width: " ~ tiles_grid_width.to!string ~ "\n";
1553 		ret ~= "tiles_border_size: " ~ tiles_border_size.to!string ~ "\n";
1554 
1555 		ret ~= "==== TILES ====\n";
1556 		ret ~= tiles.map!(a => a.dump()).join;
1557 
1558 		ret ~= "==== ISLANDS ====\n";
1559 		ret ~= islands.map!(a => a.dump()).join;
1560 
1561 		ret ~= "==== ISLAND PATH NODES ====\n";
1562 		ret ~= islands_path_nodes.map!(a => format!"ISPN next: %s, _padding %s, weight: %s\n"(a.next, a._padding, a.weight)).join;
1563 
1564 		return ret;
1565 	}
1566 
1567 
1568 
1569 	// Each entry is one triangle index from every separate island on this tile
1570 	private static struct IslandMeta{
1571 		uint32_t tile;
1572 		uint32_t islandTriangle;
1573 		// This will store all edges that can lead to other tiles
1574 		uint32_t[] edges;
1575 	}
1576 
1577 	/**
1578 	Removes triangles from the mesh, and removes unused vertices and edges accordingly.
1579 
1580 	Also updates vertex / edge / triangle indices to match new indices.
1581 
1582 	Does not updates path tables. You need to run `bake()` to re-generate path tables.
1583 
1584 	Params:
1585 	removeFunc = Delegate to check is triangle must be removed.
1586 	*/
1587 	void removeTriangles(bool delegate(in Triangle) removeFunc){
1588 		uint32_t[] vertTransTable, edgeTransTable, triTransTable;
1589 		vertTransTable.length = vertices.length;
1590 		edgeTransTable.length = edges.length;
1591 		triTransTable.length = triangles.length;
1592 		vertTransTable[] = uint32_t.max;
1593 		edgeTransTable[] = uint32_t.max;
1594 		triTransTable[] = uint32_t.max;
1595 
1596 		bool[] usedEdges, usedVertices;
1597 		usedVertices.length = vertices.length;
1598 		usedEdges.length = edges.length;
1599 		usedVertices[] = false;
1600 		usedEdges[] = false;
1601 
1602 		// Reduce triangle list & flag used edges
1603 		uint32_t newIndex = 0;
1604 		foreach(i, ref triangle ; triangles){
1605 			if(removeFunc(triangle)){
1606 
1607 				// Flag used / unused vertices & edges
1608 				foreach(vert ; triangle.vertices){
1609 					usedVertices[vert] = true;
1610 				}
1611 				foreach(edge ; triangle.linked_edges){
1612 					if(edge != uint32_t.max)
1613 						usedEdges[edge] = true;
1614 				}
1615 
1616 				// Reduce triangle list in place
1617 				triangles[newIndex] = triangle;
1618 				triTransTable[i] = newIndex++;
1619 			}
1620 			else
1621 				triTransTable[i] = uint32_t.max;
1622 		}
1623 		triangles.length = newIndex;
1624 
1625 		// Reduce vertices list
1626 		newIndex = 0;
1627 		foreach(i, used ; usedVertices){
1628 			if(used){
1629 				vertices[newIndex] = vertices[i];
1630 				vertTransTable[i] = newIndex++;
1631 			}
1632 			else
1633 				vertTransTable[i] = uint32_t.max;
1634 		}
1635 		vertices.length = newIndex;
1636 
1637 		// Reduce edges list
1638 		newIndex = 0;
1639 		foreach(i, used ; usedEdges){
1640 			if(used){
1641 				edges[newIndex] = edges[i];
1642 				edgeTransTable[i] = newIndex++;
1643 			}
1644 			else
1645 				edgeTransTable[i] = uint32_t.max;
1646 		}
1647 		edges.length = newIndex;
1648 
1649 		translateIndices(triTransTable, edgeTransTable, vertTransTable);
1650 	}
1651 
1652 	/**
1653 	Translate triangle / edge / vertex indices stored in mesh data.
1654 
1655 	Each argument is a table of the length of the existing list where:
1656 	<ul>
1657 	<li>The index is the index of the current triangle</li>
1658 	<li>The value is the index of the translated triangle</li>
1659 	</ul>
1660 	If the argument is an empty array, no translation is done. Does NOT update path tables & islands data.
1661 	*/
1662 	void translateIndices(uint32_t[] triTransTable, uint32_t[] edgeTransTable, uint32_t[] vertTransTable){
1663 		immutable ttrans = triTransTable.length > 0;
1664 		immutable jtrans = edgeTransTable.length > 0;
1665 		immutable vtrans = vertTransTable.length > 0;
1666 
1667 		// Adjust indices in edges data
1668 		foreach(ref edge ; edges){
1669 			if(vtrans){
1670 				foreach(ref vert ; edge.vertices){
1671 					vert = vertTransTable[vert];
1672 					assert(vert != uint32_t.max && vert < vertices.length, "Invalid vertex index");
1673 				}
1674 			}
1675 			if(ttrans){
1676 				foreach(ref tri ; edge.triangles){
1677 					if(tri != uint32_t.max){
1678 						tri = triTransTable[tri];
1679 						assert(tri == uint32_t.max || tri < triangles.length, "Invalid triangle index");
1680 					}
1681 				}
1682 
1683 			}
1684 			// Pack triangle indices (may be overkill)
1685 			if(edge.triangles[0] == uint32_t.max && edge.triangles[1] != uint32_t.max){
1686 				edge.triangles[0] = edge.triangles[1];
1687 				edge.triangles[1] = uint32_t.max;
1688 			}
1689 		}
1690 
1691 		// Adjust indices in triangles data
1692 		foreach(ref triangle ; triangles){
1693 			if(vtrans){
1694 				foreach(ref vert ; triangle.vertices){
1695 					vert = vertTransTable[vert];
1696 					assert(vert != uint32_t.max && vert < vertices.length, "Invalid vertex index");
1697 				}
1698 			}
1699 			if(jtrans){
1700 				foreach(ref edge ; triangle.linked_edges){
1701 					edge = edgeTransTable[edge];//All triangles should have 3 edges
1702 					assert(edge < edges.length, "Invalid edge index");
1703 				}
1704 			}
1705 			if(ttrans){
1706 				foreach(ref tri ; triangle.linked_triangles){
1707 					if(tri != uint32_t.max){
1708 						tri = triTransTable[tri];
1709 					}
1710 				}
1711 			}
1712 		}
1713 
1714 	}
1715 
1716 	/// Reorder triangles and prepare tile triangles associations
1717 	private
1718 	void splitTiles(){
1719 		uint32_t[] triTransTable;
1720 		triTransTable.length = triangles.length;
1721 		triTransTable[] = uint32_t.max;
1722 
1723 
1724 		Triangle[] newTriangles;
1725 		newTriangles.length = triangles.length;
1726 		uint32_t newTrianglesPtr = 0;
1727 
1728 		foreach(y ; 0 .. tiles_grid_height){
1729 			foreach(x ; 0 .. tiles_grid_width){
1730 				auto tileAABB = box2f(
1731 					vec2f(x * tiles_width,       y * tiles_width),
1732 					vec2f((x + 1) * tiles_width, (y + 1) * tiles_width));
1733 
1734 				auto tile = &tiles[y * tiles_grid_width + x];
1735 				tile.header.triangles_offset = newTrianglesPtr;
1736 
1737 				foreach(i, ref tri ; triangles){
1738 					if(tileAABB.contains(vec2f(tri.center))){
1739 						newTriangles[newTrianglesPtr] = tri;
1740 						triTransTable[i] = newTrianglesPtr;
1741 						newTrianglesPtr++;
1742 					}
1743 				}
1744 				tile.header.triangles_count = newTrianglesPtr - tile.header.triangles_offset;
1745 			}
1746 		}
1747 
1748 		triangles = newTriangles[0 .. newTrianglesPtr];
1749 
1750 		translateIndices(triTransTable, [], []);
1751 	}
1752 
1753 	/**
1754 	Bake the existing walkmesh by re-creating tiles, islands, path tables, ...
1755 
1756 	Does not modify the current walkmesh like what you would expect with
1757 	placeable walkmesh / walkmesh cutters.
1758 
1759 	Params:
1760 	removeBorders = true to remove unwalkable map borders from the walkmesh.
1761 	*/
1762 	void bake(bool removeBorders = true){
1763 		// Reset island associations
1764 		triangles.each!((ref a) => a.island = 0xffff);
1765 
1766 		// Remove border triangles
1767 		if(removeBorders){
1768 			auto terrainAABB = box2f(
1769 				vec2f(tiles_border_size * tiles_width, tiles_border_size * tiles_width),
1770 				vec2f((tiles_grid_width - tiles_border_size) * tiles_width, (tiles_grid_height - tiles_border_size) * tiles_width));
1771 
1772 			removeTriangles(a => terrainAABB.contains(vec2f(a.center)));
1773 		}
1774 
1775 		// Reorder triangles to have consecutive triangles for each tile
1776 		splitTiles();
1777 
1778 		IslandMeta[] islandsMeta;
1779 		islandsMeta.reserve(tiles.length * 2);
1780 
1781 		// Bake tiles
1782 		foreach(i ; 0 .. tiles.length){
1783 			//removeBorders
1784 			islandsMeta ~= bakeTile(i.to!uint32_t);
1785 		}
1786 
1787 		// islandTileID looks random-ish in TRX files. Here we generate by
1788 		// calculating a 32bit CRC with islandsMeta data, so bake() result is
1789 		// reproducible
1790 		import std.digest.crc: crc32Of;
1791 		auto islandTileID = *cast(uint32_t*)crc32Of(islandsMeta).ptr;
1792 
1793 		islands.length = islandsMeta.length;
1794 		foreach(i, ref island ; islands){
1795 			// Set island index
1796 			island.header.index = i.to!uint32_t;
1797 
1798 			// Set island associated tile
1799 			//island.header.tile = islandsMeta[i].tile;
1800 			island.header.tile = islandTileID;
1801 
1802 			auto tile = &tiles[islandsMeta[i].tile];
1803 			auto tileTriangleOffset = tile.header.triangles_offset;
1804 			auto firstLTri = islandsMeta[i].islandTriangle - tileTriangleOffset;
1805 			auto tileNTLLen = tile.path_table.node_to_local.length;
1806 			auto nodeIndex = tile.path_table.local_to_node[firstLTri];
1807 
1808 			assert(nodeIndex != 0xff, "BakeTile returned a non walkable islandTriangle");
1809 
1810 			//writeln("len=", tile.path_table.nodes.length, " [", tileNTLLen * nodeIndex, " .. ", tileNTLLen * (nodeIndex + 1), "], ntllen=", tileNTLLen);
1811 			auto nodes = tile.path_table.nodes[tileNTLLen * nodeIndex .. tileNTLLen * (nodeIndex + 1)];
1812 
1813 			// Retrieve island triangle list
1814 			uint32_t[] islandTriangles;
1815 			islandTriangles.reserve(nodes.length);
1816 
1817 			islandTriangles ~= islandsMeta[i].islandTriangle;
1818 			foreach(j, node ; nodes){
1819 				// TODO: o(n^^2)
1820 				if(node != 0xff)
1821 					islandTriangles ~= (tile.path_table.local_to_node.countUntil(j) + tileTriangleOffset).to!uint32_t;
1822 			}
1823 
1824 			// Set island triangle count
1825 			island.header.triangles_count = islandTriangles.length.to!uint32_t;
1826 
1827 			// Set island center (calculated by avg all triangle centers)
1828 			island.header.center.position = [0,0,0];
1829 			foreach(t ; islandTriangles)
1830 				island.header.center.position[0 .. 2] += triangles[t].center[];
1831 			island.header.center.position[] /= cast(double)islandTriangles.length;
1832 
1833 			// Set triangle associated island index
1834 			foreach(t ; islandTriangles)
1835 				triangles[t].island = i.to!uint16_t;
1836 		}
1837 
1838 		// Set island connections
1839 		foreach(i, ref island ; islands){
1840 
1841 			island.adjacent_islands.length = 0;
1842 			island.adjacent_islands_dist.length = 0;
1843 			island.exit_triangles.length = 0;
1844 
1845 			foreach(edge ; islandsMeta[i].edges){
1846 
1847 				uint32_t exitTriangle = uint32_t.max;
1848 				uint32_t exitIsland = uint32_t.max;
1849 
1850 				foreach(t ; edges[edge].triangles){
1851 					immutable islandIdx = triangles[t].island;
1852 					if(islandIdx == i)
1853 						exitTriangle = t;
1854 					else
1855 						exitIsland = islandIdx;
1856 				}
1857 
1858 				if(exitTriangle != uint32_t.max && exitIsland != uint32_t.max
1859 				&& island.adjacent_islands.find(exitIsland).empty){
1860 					island.adjacent_islands ~= exitIsland;
1861 					island.exit_triangles ~= exitTriangle;
1862 
1863 					// Calculate island distance
1864 					import std.math: sqrt;
1865 					auto dist = islands[exitIsland].header.center.position.dup;
1866 					dist[] -= island.header.center.position[];
1867 					island.adjacent_islands_dist ~= sqrt(dist[0] ^^ 2 + dist[1] ^^ 2);
1868 				}
1869 
1870 			}
1871 		}
1872 
1873 		// Rebuild island path tables
1874 		islands_path_nodes.length = islands.length ^^ 2;
1875 		islands_path_nodes[] = IslandPathNode(uint16_t.max, 0, 0.0);
1876 
1877 
1878 		foreach(fromIslandIdx, ref fromIsland ; islands){
1879 
1880 			bool[] visitedIslands;
1881 			visitedIslands.length = islands.length;
1882 			visitedIslands[] = false;
1883 
1884 
1885 			static struct NextToExplore{
1886 				uint16_t[] list;
1887 				uint16_t target = uint16_t.max;
1888 				float distance = 0.0;
1889 			}
1890 			auto getIslandPathNode(uint32_t from, uint32_t to){
1891 				return &islands_path_nodes[from * islands.length + to];
1892 			}
1893 
1894 			NextToExplore[] explore(uint16_t islandIdx, uint16_t targetIsland = uint16_t.max, float distance = 0.0){
1895 				NextToExplore[] ret;
1896 				if(targetIsland != uint16_t.max)
1897 					ret ~= NextToExplore([], targetIsland, distance);
1898 
1899 
1900 				foreach(j, linkedIslIdx ; islands[islandIdx].adjacent_islands){
1901 
1902 					if(linkedIslIdx == fromIslandIdx)
1903 						continue;// We must not visit initial island (node value must stay as 0xff)
1904 
1905 					auto linkedIsl = &islands[linkedIslIdx];
1906 
1907 					auto node = getIslandPathNode(cast(uint32_t)fromIslandIdx, linkedIslIdx);
1908 					if(node.next == uint16_t.max){
1909 						// This is the first time we visit the island from this fromTriIdx
1910 
1911 						if(targetIsland == uint16_t.max){
1912 							ret ~= NextToExplore([], linkedIslIdx.to!uint16_t, islands[islandIdx].adjacent_islands_dist[j]);
1913 						}
1914 
1915 						ret[$-1].list ~= linkedIslIdx.to!uint16_t;
1916 
1917 						node.next = ret[$-1].target;
1918 						node.weight = ret[$-1].distance;
1919 					}
1920 				}
1921 				return ret;
1922 			}
1923 
1924 			NextToExplore[] nextToExplore = [ NextToExplore([fromIslandIdx.to!uint16_t]) ];
1925 			NextToExplore[] newNextToExplore;
1926 			while(nextToExplore.length > 0 && nextToExplore.map!(a => a.list.length).sum > 0){
1927 				foreach(ref nte ; nextToExplore){
1928 					foreach(t ; nte.list){
1929 						newNextToExplore ~= explore(t, nte.target, nte.distance);
1930 					}
1931 				}
1932 				nextToExplore = newNextToExplore;
1933 				newNextToExplore.length = 0;
1934 			}
1935 
1936 		}
1937 
1938 		debug validate();
1939 	}
1940 
1941 	/// Bake tile path table
1942 	private IslandMeta[] bakeTile(uint32_t tileIndex){
1943 		//writeln("bakeTile: ", tileIndex);
1944 
1945 		auto tile = &tiles[tileIndex];
1946 		uint32_t tileX = tileIndex % tiles_grid_width;
1947 		uint32_t tileY = tileIndex / tiles_grid_width;
1948 
1949 		// Get tile bounding box
1950 		auto tileAABB = box2f(
1951 			vec2f(tileX * tiles_width, tileY * tiles_width),
1952 			vec2f((tileX + 1) * tiles_width, (tileY + 1) * tiles_width));
1953 
1954 		// Build tile triangle list
1955 		immutable trianglesOffset = tile.header.triangles_offset;
1956 		uint32_t[] tileTriangles;
1957 		tileTriangles.length = tile.header.triangles_count;
1958 		foreach(i, ref t ; tileTriangles)
1959 			t = (i + trianglesOffset).to!uint32_t;
1960 
1961 		// Recalculate edge & vert count
1962 		tile.header.edges_count = triangles[trianglesOffset .. trianglesOffset + tile.header.triangles_count]
1963 			.map!((ref a) => a.linked_edges[])
1964 			.join
1965 			.filter!(a => a != a.max)
1966 			.array
1967 			.sort
1968 			.uniq
1969 			.array.length.to!uint32_t;
1970 		tile.header.vertices_count = triangles[trianglesOffset .. trianglesOffset + tile.header.triangles_count]
1971 			.map!((ref a) => a.vertices[])
1972 			.join
1973 			.filter!(a => a != a.max)
1974 			.array
1975 			.sort
1976 			.uniq
1977 			.array.length.to!uint32_t;
1978 
1979 
1980 		// Find walkable triangles to deduce NTL length & LTN content
1981 		const walkableTriangles = tileTriangles.filter!(a => triangles[a].flags & Triangle.Flags.walkable).array;
1982 		immutable walkableTrianglesLen = walkableTriangles.length.to!uint32_t;
1983 
1984 		// node_to_local indices are stored on 7 bits
1985 		enforce(walkableTrianglesLen < 0b0111_1111, "Too many walkable triangles on a single tile");
1986 
1987 		// Fill NTL with walkable triangles local indices
1988 		tile.path_table.node_to_local = walkableTriangles.dup;
1989 		tile.path_table.node_to_local[] -= trianglesOffset;
1990 		ubyte getNtlIndex(uint32_t destTriangle){
1991 			destTriangle -= trianglesOffset;
1992 			// insert destTriangle inside ntl and return its index
1993 			foreach(i, t ; tile.path_table.node_to_local){
1994 				if(t == destTriangle)
1995 					return i.to!ubyte;
1996 			}
1997 			assert(0, "Triangle local idx="~destTriangle.to!string~" not found in NTL array "~tile.path_table.node_to_local.to!string);
1998 		}
1999 
2000 		// Set LTN content: 0xff if the triangle is unwalkable, otherwise an
2001 		// index in walkableTriangles
2002 		tile.path_table.local_to_node.length = tile.header.triangles_count;
2003 
2004 		tile.path_table.local_to_node[] = 0xff;
2005 		foreach(i, triIdx ; walkableTriangles)
2006 			tile.path_table.local_to_node[triIdx - trianglesOffset] = i.to!ubyte;
2007 
2008 		// Resize nodes table
2009 		tile.path_table.nodes.length = (walkableTrianglesLen * walkableTrianglesLen).to!uint32_t;
2010 		tile.path_table.nodes[] = 0xff;// 0xff means inaccessible.
2011 
2012 
2013 		ubyte* getNode(uint32_t fromGIdx, uint32_t toGIdx) {
2014 			return &tile.path_table.nodes[
2015 				tile.path_table.local_to_node[fromGIdx - trianglesOffset] * walkableTrianglesLen
2016 				+ tile.path_table.local_to_node[toGIdx - trianglesOffset]
2017 			];
2018 		}
2019 
2020 
2021 		// Visited triangles. Not used for pathfinding, but for island detection.
2022 		bool[] visitedTriangles;
2023 		visitedTriangles.length = tile.path_table.local_to_node.length;
2024 		visitedTriangles[] = false;
2025 
2026 
2027 		IslandMeta[] islandsMeta;
2028 		bool islandRegistration = false;
2029 
2030 
2031 		// Calculate pathfinding
2032 		foreach(i, fromTriIdx ; walkableTriangles){
2033 
2034 			// If the triangle has not been visited before, we add a new
2035 			// island All triangles accessible from this one will be marked as
2036 			// visited, so we don't add more than once the same island
2037 			if(visitedTriangles[fromTriIdx - trianglesOffset] == false){
2038 				islandsMeta ~= IslandMeta(tileIndex, fromTriIdx, []);
2039 				islandRegistration = true;
2040 			}
2041 			else
2042 				islandRegistration = false;
2043 
2044 			float[] costTable;
2045 			costTable.length = tile.path_table.local_to_node.length;
2046 			costTable[] = float.infinity;
2047 
2048 			static struct NextToExplore{
2049 				Tuple!(uint32_t, float)[] list;
2050 				ubyte ntlTarget = ubyte.max;
2051 			}
2052 
2053 			NextToExplore[] explore(uint32_t currTriIdx, float currCost, ubyte ntlTarget = ubyte.max){
2054 				//TODO: currently the fastest route is the route that cross
2055 				//the minimum number of triangles, which isn't always true. We
2056 				//need to take the distance between triangles into account
2057 
2058 				NextToExplore[] ret;
2059 				if(ntlTarget != ubyte.max)
2060 					ret ~= NextToExplore([], ntlTarget);
2061 
2062 				foreach(j, linkedTriIdx ; triangles[currTriIdx].linked_triangles){
2063 					if(linkedTriIdx == uint32_t.max)
2064 						continue;// there is no linked triangle
2065 
2066 					if(fromTriIdx == linkedTriIdx)
2067 						continue;// We must not visit initial triangle (node value must stay as 0xff)
2068 
2069 					auto linkedTri = &triangles[linkedTriIdx];
2070 
2071 					if(!(linkedTri.flags & linkedTri.Flags.walkable))
2072 						continue;// non walkable triangle
2073 
2074 					if(tileAABB.contains(vec2f(linkedTri.center))){
2075 						// linkedTri is inside the tile
2076 
2077 						// Mark the triangle as visited (only for island detection)
2078 						visitedTriangles[linkedTriIdx - trianglesOffset] = true;
2079 
2080 						float cost = currCost + vec2f(triangles[currTriIdx].center).distanceTo(vec2f(linkedTri.center));
2081 
2082 						if(cost < costTable[linkedTriIdx - trianglesOffset]){
2083 							costTable[linkedTriIdx - trianglesOffset] = cost;
2084 
2085 							auto node = getNode(fromTriIdx, linkedTriIdx);
2086 							if(ntlTarget == ubyte.max){
2087 								ret ~= NextToExplore([], getNtlIndex(linkedTriIdx));
2088 							}
2089 
2090 							ret[$-1].list ~= Tuple!(uint32_t, float)(linkedTriIdx, cost);
2091 
2092 							assert(ret[$-1].ntlTarget < 0b0111_1111);
2093 							*node = ret[$-1].ntlTarget;// TODO: do VISIBLE / LOS calculation
2094 						}
2095 
2096 					}
2097 					else{
2098 						// linkedTri is outside the tile
2099 						if(islandRegistration){
2100 							immutable edgeIdx = triangles[currTriIdx].linked_edges[j];
2101 							assert(edges[edgeIdx].triangles[0] == currTriIdx && edges[edgeIdx].triangles[1] == linkedTriIdx
2102 								|| edges[edgeIdx].triangles[1] == currTriIdx && edges[edgeIdx].triangles[0] == linkedTriIdx,
2103 								"Incoherent edge "~edgeIdx.to!string~": "~edges[edgeIdx].to!string);
2104 							islandsMeta[$-1].edges ~= edgeIdx;
2105 						}
2106 					}
2107 				}
2108 				return ret;
2109 			}
2110 
2111 			NextToExplore[] nextToExplore = [ NextToExplore([Tuple!(uint32_t,float)(fromTriIdx, 0)]) ];
2112 			NextToExplore[] newNextToExplore;
2113 			while(nextToExplore.length > 0 && nextToExplore.map!(a => a.list.length).sum > 0){
2114 				foreach(ref nte ; nextToExplore){
2115 					foreach(t ; nte.list){
2116 						newNextToExplore ~= explore(t[0], t[1], nte.ntlTarget);
2117 					}
2118 				}
2119 				nextToExplore = newNextToExplore;
2120 				newNextToExplore.length = 0;
2121 			}
2122 		}
2123 
2124 		//if(walkableTrianglesLen > 0)
2125 		//	writeln("Tile ", tileIndex, ": ", walkableTrianglesLen, " walkable triangles in ", islandsMeta.length, " islands");
2126 		return islandsMeta;
2127 	}
2128 
2129 	/// Set the footstep sound flags for each triangle of the walkmesh
2130 	/// Params:
2131 	/// trrnPackets = TRN packet list containing the needed `TrnNWN2MegatilePayload` packets
2132 	/// textureFlags = Associative array listing all textures and their respective footstep sounds
2133 	void setFootstepSounds(in TrnPacket[] trrnPackets, in Triangle.Flags[string] textureFlags){
2134 		import nwn.dds;
2135 
2136 		// Prepare megatile info for quick access
2137 		static struct Megatile {
2138 			box2f aabb;
2139 			string[6] textures;
2140 			Dds[2] dds;
2141 		}
2142 		Megatile[] megatiles;
2143 		foreach(ref packet ; trrnPackets){
2144 			if(packet.type == TrnPacketType.NWN2_TRRN){
2145 				with(packet.as!(TrnPacketType.NWN2_TRRN)){
2146 					Megatile megatile;
2147 
2148 					auto min = vec2f(float.infinity, float.infinity);
2149 					auto max = vec2f(-float.infinity, -float.infinity);
2150 					foreach(ref v ; vertices){
2151 						if(v.position[0] < min.x || v.position[1] < min.y) min = vec2f(v.position[0..2]);
2152 						if(v.position[0] > max.x || v.position[1] > max.y) max = vec2f(v.position[0..2]);
2153 					}
2154 					megatile.aabb = box2f(min, max);
2155 
2156 					foreach(i ; 0 .. 6)
2157 						megatile.textures[i] = textures[i].name.ptr.fromStringz.idup;
2158 
2159 					megatile.dds[0] = Dds(dds_a);
2160 					megatile.dds[1] = Dds(dds_b);
2161 
2162 					megatiles ~= megatile;
2163 				}
2164 			}
2165 		}
2166 
2167 		foreach(i, ref t ; triangles){
2168 			bool found = false;
2169 			// TODO: sort these megatiles by row so we can find the megatile for any triangle with o(1)
2170 			foreach(ref mt ; megatiles){
2171 				if(mt.aabb.contains(cast(vec2f)t.center)){
2172 					found = true;
2173 
2174 					auto pos = vec2i(((t.center - mt.aabb.min) * mt.dds[0].header.width / mt.aabb.size.magnitude)[].to!(int[]));
2175 					auto p0 = mt.dds[0].getPixel(pos.x, pos.y);
2176 					auto p1 = mt.dds[1].getPixel(pos.x, pos.y);
2177 
2178 					ubyte maxTextureIdx = ubyte.max;
2179 					int maxTextureIntensity = -1;
2180 					foreach(textureIdx, intensity ; p0[0 .. 4] ~ p1[0 .. 2]){
2181 						if(mt.textures[textureIdx].length > 0 && intensity > maxTextureIntensity){
2182 							maxTextureIdx = cast(ubyte)textureIdx;
2183 							maxTextureIntensity = intensity;
2184 						}
2185 					}
2186 					assert(maxTextureIdx != ubyte.max, "No texture found");
2187 
2188 					if(auto flag = mt.textures[maxTextureIdx] in textureFlags){
2189 						t.flags &= t.flags.max ^ Triangle.Flags.soundstepFlags;
2190 						t.flags |= *flag;
2191 					}
2192 					else
2193 						assert(0, "Texture '"~mt.textures[maxTextureIdx]~"' not in texture/flag list");
2194 
2195 					break;
2196 				}
2197 			}
2198 			enforce(found, "No megatile found for triangle "~i.to!string);
2199 		}
2200 	}
2201 
2202 	/// ditto
2203 	/// Params:
2204 	/// trrnPackets = TRN packet list containing the needed `TrnNWN2MegatilePayload` packets
2205 	/// terrainmaterials = terrainmaterials.2da that lists all textures and their respective footstep sounds
2206 	void setFootstepSounds(in TrnPacket[] trrnPackets, in TwoDA terrainmaterials){
2207 		// Find soundstep flags for each
2208 		Triangle.Flags[string] textureFlags;
2209 		immutable materialColIdx = terrainmaterials.columnIndex("Material");
2210 		foreach(i ; 0 .. terrainmaterials.rows){
2211 
2212 			Triangle.Flags flag;
2213 			switch(terrainmaterials.get("Material", i)){
2214 				case "Dirt":    flag = Triangle.Flags.dirt; break;
2215 				case "Grass":   flag = Triangle.Flags.grass; break;
2216 				case "Rock",
2217 				     "Stone":   flag = Triangle.Flags.stone; break;
2218 				case "Wood":    flag = Triangle.Flags.wood; break;
2219 				case "Carpet":  flag = Triangle.Flags.carpet; break;
2220 				case "Metal":   flag = Triangle.Flags.metal; break;
2221 				case "Swamp":   flag = Triangle.Flags.swamp; break;
2222 				case "Mud":     flag = Triangle.Flags.mud; break;
2223 				case "Leaves":  flag = Triangle.Flags.leaves; break;
2224 				case "Water":   flag = Triangle.Flags.water; break;
2225 				case "Puddles": flag = Triangle.Flags.puddles; break;
2226 				case null:      continue;
2227 				default: assert(0, "Unknown terrain material type '"~terrainmaterials.get("Material", i)~"' in "~terrainmaterials.fileName);
2228 			}
2229 			textureFlags[terrainmaterials.get("Terrain", i)] = flag;
2230 		}
2231 
2232 		setFootstepSounds(trrnPackets, textureFlags);
2233 	}
2234 
2235 	/**
2236 	Calculate the fastest route between two islands. The area need to be baked, as it uses existing path tables.
2237 	*/
2238 	uint16_t[] findIslandsPath(in uint16_t fromIslandIndex, in uint16_t toIslandIndex) const {
2239 		uint16_t from = fromIslandIndex;
2240 		int iSec = 0;
2241 		uint16_t[] ret;
2242 		while(fromIslandIndex != toIslandIndex && iSec++ < 1000){
2243 			auto node = &islands_path_nodes[from * islands.length + toIslandIndex];
2244 			if(node.next == uint16_t.max)
2245 				return ret;
2246 
2247 			from = node.next;
2248 			ret ~= from;
2249 		}
2250 		assert(iSec < 1000, "Islands precalculated paths lead to a loop (from="~fromIslandIndex.to!string~", to="~toIslandIndex.to!string~")");
2251 		return ret;
2252 	}
2253 
2254 	/**
2255 	Set 3d mesh geometry
2256 	*/
2257 	void setGenericMesh(in GenericMesh mesh){
2258 		// Copy vertices
2259 		vertices.length = mesh.vertices.length;
2260 		foreach(i, ref v ; vertices)
2261 			v.position = mesh.vertices[i].v;
2262 
2263 		// Copy triangles
2264 		triangles.length = mesh.triangles.length;
2265 		foreach(i, ref t ; triangles){
2266 			t.vertices = mesh.triangles[i].vertices.dup[0 .. 3];
2267 
2268 			t.linked_edges[] = uint32_t.max;
2269 			t.linked_triangles[] = uint32_t.max;
2270 
2271 			t.center = vertices[t.vertices[0]].position[0 .. 2];
2272 			t.center[] += vertices[t.vertices[1]].position[0 .. 2];
2273 			t.center[] += vertices[t.vertices[2]].position[0 .. 2];
2274 			t.center[] /= 3.0;
2275 
2276 			t.normal = (mesh.vertices[t.vertices[1]] - mesh.vertices[t.vertices[0]])
2277 				.cross(mesh.vertices[t.vertices[2]] - mesh.vertices[t.vertices[0]]).normalized[0..3];
2278 
2279 			t.dot_product = -dot(vec3f(t.normal), mesh.vertices[t.vertices[0]]);
2280 
2281 			t.island = uint16_t.max;
2282 
2283 			t.flags |= t.Flags.walkable;
2284 			if(isTriangleClockwise(t.vertices[].map!(a => vec2f(vertices[a].position[0 .. 2])).array[0 .. 3]))
2285 				t.flags |= t.Flags.clockwise;
2286 			else
2287 				t.flags &= t.flags.max ^ t.Flags.clockwise;
2288 		}
2289 
2290 		// Rebuild edge list
2291 		buildEdges();
2292 	}
2293 
2294 	/**
2295 	Converts terrain mesh data to a more generic format.
2296 
2297 	Params:
2298 	triangleFlags = Triangle flags to include in the generic mesh.
2299 	Set to `uint16_t.max` to include all triangles.
2300 	*/
2301 	GenericMesh toGenericMesh(uint16_t triangleFlags = Triangle.Flags.walkable) const {
2302 		GenericMesh ret;
2303 		ret.vertices.length = vertices.length;
2304 		ret.triangles.length = triangles.length;
2305 
2306 		foreach(i, ref v ; vertices){
2307 			ret.vertices[i] = vec3f(v.position);
2308 		}
2309 
2310 		size_t ptr = 0;
2311 		foreach(i, ref t ; triangles){
2312 			if(triangleFlags == uint16_t.max || t.flags & triangleFlags){
2313 				debug foreach(j, v ; t.vertices)
2314 					assert(v < ret.vertices.length, format!"Vertex %d (index=%d) of triangle %d is out of range. Please check mesh with Validate() before conversion."(j, v, i));
2315 				ret.triangles[ptr++] = GenericMesh.Triangle(t.vertices);
2316 			}
2317 		}
2318 		ret.triangles.length = ptr;
2319 		return ret;
2320 	}
2321 
2322 	/**
2323 	Rebuilds edge data by going through every triangle / vertices
2324 
2325 	Warning: NWN2 official baking tool often produces duplicated triangles and
2326 	edges around placeable walkmeshes.
2327 	*/
2328 	void buildEdges(){
2329 		uint32_t[uint32_t[2]] edgeMap;
2330 		uint32_t findEdge(uint32_t[2] vertices){
2331 			if(auto j = vertices in edgeMap)
2332 				return *j;
2333 			return uint32_t.max;
2334 		}
2335 
2336 		edges.length = 0;
2337 
2338 		foreach(i, ref t ; triangles){
2339 			// Create edges as needed
2340 			foreach(j ; 0 .. 3){
2341 				auto vrt = [t.vertices[j], t.vertices[(j+1) % 3]].sort.array;
2342 				auto edgeIdx = findEdge(vrt[0 .. 2]);
2343 
2344 				if(edgeIdx == uint32_t.max){
2345 					// Add new edge
2346 					edgeMap[vrt[0 .. 2]] = edges.length.to!uint32_t;
2347 					edges ~= Edge(vrt[0 .. 2], [i.to!uint32_t, uint32_t.max]);
2348 				}
2349 				else{
2350 					// Add triangle to existing edge
2351 					enforce(edges[edgeIdx].triangles[1] == uint32_t.max,
2352 						"Edge "~edgeIdx.to!string~" = "~edges[edgeIdx].to!string~" cannot be linked to more than 2 triangles (cannot add triangle "~i.to!string~")");
2353 					edges[edgeIdx].triangles[1] = i.to!uint32_t;
2354 				}
2355 			}
2356 		}
2357 
2358 		// update triangles[].linked_edge & triangles[].linked_triangles
2359 		foreach(edgeIdx, ref edge ; edges){
2360 			assert(edge.triangles[0] != uint32_t.max);
2361 
2362 			foreach(j, tIdx ; edge.triangles){
2363 				if(tIdx == uint32_t.max)
2364 					continue;
2365 
2366 				size_t slot;
2367 				for(slot = 0 ; slot < 3 ; slot++)
2368 					if(triangles[tIdx].linked_edges[slot] == uint32_t.max)
2369 						break;
2370 				assert(slot < 3, "Triangle "~tIdx.to!string~" is already linked to 3 triangles");
2371 
2372 				triangles[tIdx].linked_edges[slot] = edgeIdx.to!uint32_t;
2373 				triangles[tIdx].linked_triangles[slot] = edge.triangles[(j + 1) % 2];
2374 			}
2375 		}
2376 
2377 	}
2378 }
2379 
2380 unittest {
2381 	auto epportesTrx = cast(ubyte[])import("eauprofonde-portes.trx");
2382 
2383 	auto trn = new Trn(epportesTrx);
2384 	auto serialized = trn.serialize();
2385 	assert(epportesTrx.length == serialized.length && epportesTrx == serialized);
2386 
2387 	auto terrainmaterials = new TwoDA(cast(ubyte[])import("terrainmaterials.2da"));
2388 
2389 	foreach(ref TrnNWN2WalkmeshPayload aswm ; trn){
2390 		aswm.validate();
2391 		aswm.bake();
2392 
2393 		assertThrown!Error(aswm.tiles[666].findPath(0, 1));// Triangles outside of the tile
2394 		assert(aswm.tiles[666].findPath(15421, 15417).length == 4);// on the same island
2395 		assert(aswm.tiles[666].findPath(15452, 15470).length == 7);// on the same island
2396 		assert(aswm.tiles[778].findPath(20263, 20278).length == 0);// across two islands
2397 
2398 		assert(aswm.findIslandsPath(10, 12).length == 2);
2399 		assert(aswm.findIslandsPath(0, (aswm.islands.length - 1).to!uint16_t).length == 49);
2400 
2401 		aswm.setFootstepSounds(trn.packets, terrainmaterials);
2402 		aswm.removeTriangles((in t) => (t.flags & t.Flags.walkable) == 0);
2403 
2404 		aswm.bake();
2405 		aswm.validate();
2406 	}
2407 
2408 
2409 	trn = new Trn(cast(ubyte[])import("IslandsTest.trn"));
2410 	foreach(ref TrnNWN2WalkmeshPayload aswm ; trn){
2411 		aswm.validate();
2412 
2413 		aswm.bake();
2414 		aswm.validate();
2415 
2416 		assert(aswm.triangles.length == 1152);
2417 		assert(aswm.edges.length == 1776);
2418 
2419 		auto walkableTrianglesLength = aswm.triangles.count!(a => (a.flags & a.Flags.walkable) > 0);
2420 
2421 		auto mesh = aswm.toGenericMesh;
2422 
2423 		// Shuffle mesh
2424 		mesh.shuffle();
2425 		aswm.setGenericMesh(mesh);
2426 		aswm.bake();
2427 
2428 		aswm.validate();
2429 
2430 		// Values taken from trx file baked with the toolset
2431 		assert(aswm.triangles.length == walkableTrianglesLength);
2432 		assert(aswm.islands.length == 25);
2433 	}
2434 }
2435