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