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