WDT

From wowdev
Revision as of 01:14, 7 April 2021 by Varen (talk | contribs) (MSLT (Legion+))
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

WDT files specify exactly which map tiles map tiles are present in a world, if any, and can also reference a "global" WMO. They have a chunked file structure.

MPHD chunk

Contains 8 32-bit integers.

struct SMMapHeader {
   uint32_t flags;
#if ≥ Battle (8.1.0.28294)  
   uint32_t lgtFileDataID;
   uint32_t occFileDataID;
   uint32_t fogsFileDataID;
   uint32_t mpvFileDataID;
   uint32_t texFileDataID;
   uint32_t wdlFileDataID;
   uint32_t pd4FileDataID;  // PD4
#else
   uint32_t something;
   uint32_t unused[6];
#endif
} MPHD;

These are the only flags checked:

enum mphd_flags {
 wdt_uses_global_map_obj               = 0x0001, // Use global map object definition.
 adt_has_mccv                          = 0x0002, // ≥ Wrath adds color: ADT.MCNK.MCCV. with this flag every ADT in the map _must_ have MCCV chunk at least with default values, else only base texture layer is rendered on such ADTs.
 adt_has_big_alpha                     = 0x0004, // shader = 2. Decides whether to use _env terrain shaders or not: funky and if MCAL has 4096 instead of 2048(?)
 adt_has_doodadrefs_sorted_by_size_cat = 0x0008, // if enabled, the ADT's MCRF(m2 only)/MCRD chunks need to be sorted by size category
 FLAG_LIGHTINGVERTICES                 = 0x0010, // ≥ Cata adds second color: ADT.MCNK.MCLV -- This appears to be deprecated and forbidden in 8.x?
 adt_has_upside_down_ground            = 0x0020, // ≥ Cata Flips the ground display upside down to create a ceiling
 unk_0x0040                            = 0x0040, // ≥ Mists ??? -- Only found on Firelands2.wdt (but only since MoP) before Legion
 adt_has_height_texturing              = 0x0080, // ≥ Mists shader = 6. Decides whether to influence alpha maps by _h+MTXP: (without with)
                                                 // also changes MCAL size to 4096 for uncompressed entries
 unk_0x0100                            = 0x0100, // ≥ Legion implicitly sets 0x8000
 wdt_has_maid                          = 0x0200, // ≥ Battle (8.1.0.28294) client will load ADT using FileDataID instead of filename formatted with "%s\\%s_%d_%d.adt"
 unk_0x0400                            = 0x0400,
 unk_0x0800                            = 0x0800,
 unk_0x1000                            = 0x1000,
 unk_0x2000                            = 0x2000,
 unk_0x4000                            = 0x4000,
 unk_0x8000                            = 0x8000, // ≥ Legion implicitly set for map ids 0, 1, 571, 870, 1116 (continents). Affects the rendering of _lod.adt. If set, or if implicitly set by 0x100, maptextures and LOD are required, or else textures further than ~533 yards will not render.

 mask_vertex_buffer_format             = adt_has_mccv | adt_has_mclv,                    // CMap::LoadWdt
 mask_render_chunk_something           = adt_has_height_texturing | adt_has_big_alpha,   // CMapArea::PrepareRenderChunk, CMapChunk::ProcessIffChunks
};

See a list here.

The second integer as of WotLK is not ignored but stores something too:

for( int i = 0; i < WDT_MPHD.something/8; i++ )
{
   WDT_MAIN[i].flags = 0;
   WDT_MAIN[i].somedata = 0;
}

The other bytes seem to be unused from what I can tell. In 6.0.1.18179 I was unable to find any code referencing something other than flags.

vertexBufferFormat = !(flags & adt_has_mccv) ? EGxVertexBufferFormat_PN
                   : flags & adt_has_mclv ? EGxVertexBufferFormat_PNC2
                   : EGxVertexBufferFormat_PNC
                   ;

mcal_size = flags & mask_render_chunk_something ? 4096 : 2048;
This section only applies to versions ≥ Battle (8.1.0.28294). Could have been changed earlier.

As preparation for the removal of namehashes, this chunk underwent a change. The 'unused' array is now used for storing FileDataID references to associated WDT files and the 'something' has been removed.

MAIN chunk

  • Map tile table. Needs to contain 64x64 = 4096 entries of sizeof(SMAreaInfo) ( 8 ) bytes each.
struct SMAreaInfo     // -> CMapAreaTableEntry
{
#if version < ?       // until they maps are split into adts
  uint32_t offset;
  uint32_t size;
#endif
#if version > ?       // beginning them being split into adts
  uint32_t Flag_HasADT : 1;
#endif
#if version ≥ Cata
  uint32_t Flag_AllWater : 1;
#endif
  uint32_t Flag_Loaded : 1;

  uint32_t asyncId;    // only set during runtime.
} map_area_info[64*64];

On Cataclysm, 2 on a tile displays "fake water" ingame. If only one "fake water tile" is activated, "fake water" will appear everywhere you don't have an ADT loaded. (seen on 4.3.4.15595)

MAID chunk

This section only applies to versions ≥ Battle (8.1.0.28294).
  • Map filedataid table. Needs to contain 64x64 = 4096 entries of sizeof(MapFileDataIDs) ( 32 ) bytes each.
struct MapFileDataIDs
{
  uint32_t rootADT; // reference to fdid of mapname_xx_yy.adt
  uint32_t obj0ADT; // reference to fdid of mapname_xx_yy_obj0.adt
  uint32_t obj1ADT; // reference to fdid of mapname_xx_yy_obj1.adt
  uint32_t tex0ADT; // reference to fdid of mapname_xx_yy_tex0.adt
  uint32_t lodADT;  // reference to fdid of mapname_xx_yy_lod.adt
  uint32_t mapTexture; // reference to fdid of mapname_xx_yy.blp
  uint32_t mapTextureN; // reference to fdid of mapname_xx_yy_n.blp
  uint32_t minimapTexture; // reference to fdid of mapxx_yy.blp
} MapFileDataIDs[64*64];

MWMO, MODF chunks

For worlds with terrain, parsing ends here. If it has none, there is one MWMO and one MODF chunk here. The MODF chunk is limited to one entry. See the ADT format description for details.

MWMO chunk

  • A filename for one WMO (world map object) that appears in this map. A zero-terminated string. 0x100 is the maximum size for this chunk due to being copied into a stack allocated array (at least in MOP)! (including \0).

MODF chunk

  • Placement information for the global WMO. 64 bytes. Only one instance is possible.
Offset 	Type 		Description
0x00 	uint32 		ID -- unused, always uses MWMO's content instead
0x04 	uint32 		unique identifier for this instance -- unused, generates uid dynamically
0x08 	3 floats 	Position (X,Y,Z)
0x14 	3 floats 	Orientation (A,B,C)
0x20 	3 floats 	Upper Extents
0x2C 	3 floats 	Lower Extents
0x38 	uint16 		Flags
0x3A    uint16          Doodad set index
0x3C 	uint16 		Name set?
0x3E 	uint16 		Padding
struct SMMapObjDef
{
  uint nameId;
  uint uniqueId;
  C3Vector pos;
  C3Vector rot;
  CAaBox extents;
  uint16 flags;
  uint16 doodadSet;
  uint16 nameSet;
  uint16 pad;
};
  • How to compute a matrix to map WMO to world coordinates

Refer to MODF(ADT)

MANM

This section only applies to version Battle (8.3.0.32272).

Temporarily during 8.3.0 PTR, root WDTs contained an MANM chunk, which appeared to be data used for navigation or scripts, often marking roads or walls. It was not parsed by the client and was removed a patch later again. The shipped files had lots of constants, so little was actually known other than the positions and an ID that nobody was able to map to something useful, which seemed to be globally (across all WDTs) unique though.

/*0x00  */  uint32_t version;                // 1
/*0x04  */  uint32_t count;
            struct {
  /*+0x000*/  uint32_t id;                   // globally unique
  /*+0x004*/  uint8_t unk_0x004[0x90];       // 0     vv probably matrices, likely all zeros are also floats
  /*+0x094*/  float unk_0x094[2];            // 1.0
  /*+0x09C*/  uint8_t unk_0x09C[0x98];       // 0
  /*+0x134*/  float unk_0x134[2];            // 1.0
  /*+0x13C*/  uint8_t unk_0x13C[0x98];       // 0
  /*+0x1D4*/  float unk_0x1D4[2];            // 1.0
  /*+0x1DC*/  uint8_t unk_0x1DC[0x10];       // 0     ^^
  /*+0x1EC*/  float unk_0x1EC;               // 1.0
  /*+0x1F0*/  uint32_t unk_0x1F0;            // 6
  /*+0x1F4*/  float unk_0x1F4;               // 1.0
  /*+0x1F8*/  uint8_t unk_0x1F8[0x8];        // 0
  /*+0x200*/  float unk_0x200;               // 1.0
  /*+0x204*/  uint32_t unk_0x204;            // 6
  /*+0x208*/  float unk_0x208;               // 1.0
  /*+0x20C*/  uint8_t unk_0x20C[0x8];        // 0
  /*+0x214*/  float unk_0x214;               // 1.0
  /*+0x218*/  uint32_t unk_0x218;            // 6
  /*+0x21C*/  uint32_t unk_0x21C;            // 1
  /*+0x220*/  uint32_t typeish;              // 0, 2, 3, 4, 6. 2 or 6: absolute positions (usually paths or walls), 3: relative positions (to what?)
  /*+0x224*/  int count2;
              struct {
    /*+0x00 */  C3Vector position;
    /*+0x0C */  C3Vector normal;            // always pointing up
  /*+0x228*/  } points[count2];
/*0x08  */  } entries[count];
This section only applies to versions ≥ SL.

In Shadowlands the feature was officially added as, revealing the name MapAnima, with the client now parsing it. Four versions have been done during development. As of

  • SL (9.0.1.33978) … 9.0.1.34199, the version is 6,
  • SL (9.0.1.34278) … 9.0.1.34821, the version is 7, and starting
  • SL (9.0.1.34972), the version is 8.
// offset comments are assuming version 8
struct Anima {
  struct Data {
    struct TIA {
      /*0x000*/ float unk_000; // AnimaMaterial_Field_9_0_1_33978_016 * AnimaMaterial_Field_9_0_1_33978_020
      /*0x004*/ float unk_004; // AnimaMaterial_Field_9_0_1_33978_017 * AnimaMaterial_Field_9_0_1_33978_021
      /*0x008*/ float unk_008; // AnimaMaterial_Field_9_0_1_33978_018 * AnimaMaterial_Field_9_0_1_33978_022
      /*0x00C*/ float AnimaMaterial_Field_9_0_1_33978_023;

      // For sake of laziness these are fdids at file time and
      // Texture* at run time, so padded to fit a x64 pointer.
      struct TextureFdidAndPadding {
        /*0x000*/ uint32_t fdid;
        /*0x004*/ char padding[4];
        /*0x008*/
      };
      /*0x010*/ TextureFdidAndPadding AnimaMaterial_EffectTexture[3];
      /*0x028*/ TextureFdidAndPadding AnimaMaterial_RibbonTexture;
      /*0x030*/ float unk_per_texture[4]; // apparently only set at runtime, to 1.0f

      /*0x040*/ float AnimaMaterial_Field_9_0_1_33978_000;
      /*0x044*/ float AnimaMaterial_Field_9_0_1_33978_001;
      /*0x048*/ float AnimaMaterial_Field_9_0_1_33978_002;
      /*0x04C*/ float AnimaMaterial_Field_9_0_1_33978_003;
      /*0x050*/ float AnimaMaterial_Field_9_0_1_33978_004;
      /*0x054*/ float AnimaMaterial_Field_9_0_1_33978_005;
      /*0x058*/ float AnimaMaterial_Field_9_0_1_33978_006;
      /*0x05C*/ float AnimaMaterial_Field_9_0_1_33978_007;
      /*0x060*/ float AnimaMaterial_Field_9_0_1_33978_008;
      /*0x064*/ float AnimaMaterial_Field_9_0_1_33978_009;
      /*0x068*/ float AnimaMaterial_Field_9_0_1_33978_010;
      /*0x06C*/ float AnimaMaterial_Field_9_0_1_33978_011;
      /*0x070*/ float AnimaMaterial_Field_9_0_1_33978_012;
      /*0x074*/ float AnimaMaterial_Field_9_0_1_33978_013;
      /*0x078*/ float AnimaMaterial_Field_9_0_1_33978_014;
      /*0x07C*/ float AnimaMaterial_Field_9_0_1_33978_015;

      /*0x080*/ C2Vector unk_080[4]; // apparently also always 0, and initialized depending on flags below

      /*0x0A0*/ uint32_t unk_0A0;
      /*0x0A4*/ float AnimaMaterial_Field_9_0_1_33978_025;
      /*0x0A8*/ float AnimaMaterial_Field_9_0_1_33978_026;
      /*0x0AC*/ uint32_t AnimaMaterial_Flags; // &4: randomize float_pairs
      /*0x0B0*/ uint32_t AnimaMaterial_ID;
      /*0x0B4*/ uint32_t AnimaMaterial_Field_9_0_1_33978_028;
      /*0x0B8*/ float AnimaMaterial_Field_9_0_1_33978_029;
      /*0x0BC*/ uint32_t unk_0BC;
      /*0x0C0*/
    };

    struct TIB {
      /*0x000*/ uint32_t a;
      /*0x004*/ uint32_t b;
      /*0x008*/ float c;
      /*0x00C*/ uint32_t slice_count;
      /*0x010*/ float d;
      /*0x014*/ uint32_t e;
      /*0x018*/
    };

    /*0x000*/ TIA tube_info_a[6];
    /*0x480*/ TIB tube_info_b[6];
    /*0x510*/ uint32_t tube_presence_mask; // if tube_info_x[i] has information, set bit 1 << i

    /*0x514*/ uint32_t AnimaCable_Field_9_0_1_33978_010; // &1: use 6 instead of 1 positions per node
    /*0x518*/ uint32_t AnimaCable_ParticleModel; // model to use particles from
    /*0x51C*/ float AnimaCable_Field_9_0_1_33978_001;
    /*0x520*/ float AnimaCable_Field_9_0_1_33978_002;
    /*0x524*/ uint32_t AnimaCable_Field_9_0_1_33978_003;
    /*0x528*/ float AnimaCable_Field_9_0_1_33978_004;
    /*0x52C*/ float AnimaCable_Field_9_0_1_33978_005;
    /*0x530*/ uint32_t AnimaCable_Field_9_0_1_33978_007;
    /*0x534*/ uint32_t AnimaCable_Field_9_0_1_33978_008;
    /*0x538*/ uint32_t AnimaCable_Field_9_0_1_33978_009;
    /*0x53C*/ float AnimaCable_Field_9_0_1_33978_027;
    /*0x540*/ float AnimaCable_Field_9_0_1_33978_028;
    /*0x544*/ float AnimaCable_Field_9_0_1_33978_029;
    /*0x548*/ float AnimaCable_Field_9_0_1_33978_030;
    /*0x54C*/ float AnimaCable_Field_9_0_1_33978_031;
    /*0x550*/ float AnimaCable_Field_9_0_1_33978_032;

    // capsules are interleaved in dbc, so mapping is a bit shit.
    // count is hardcoded to 2, which allows for that horror.
    union Capsules {
      struct {
        struct {
          /*0x000*/ float unk_000;
          /*0x004*/ float unk_004;
          /*0x008*/ float unk_008;
          /*0x00C*/ float unk_00C;
          /*0x010*/ float unk_010;
          /*0x014*/ float unk_014;
          /*0x018*/ float unk_018;
        } capsules[2];
      } logic_grouping;
      struct {
        // capsules[0]
        float AnimaCable_Field_9_0_1_33978_013;
        float AnimaCable_Field_9_0_1_33978_011;
        float AnimaCable_Field_9_0_1_33978_015;
        float AnimaCable_Field_9_0_1_33978_017;
        float AnimaCable_Field_9_0_1_33978_019;
        float AnimaCable_Field_9_0_1_33978_020;
        float AnimaCable_Field_9_0_1_33978_021;
        // capsules[1]
        float AnimaCable_Field_9_0_1_33978_014;
        float AnimaCable_Field_9_0_1_33978_012;
        float AnimaCable_Field_9_0_1_33978_016;
        float AnimaCable_Field_9_0_1_33978_018;
        float AnimaCable_Field_9_0_1_33978_022;
        float AnimaCable_Field_9_0_1_33978_023;
        float AnimaCable_Field_9_0_1_33978_024;
      } flat_like_in_db;
    };
    /*0x554*/ Capsules capsule_info;

    /*0x58C*/ uint32_t AnimaCable_Field_9_0_1_33978_006; // sound kit
    /*0x590*/ uint32_t AnimaCable_ID;
    /*0x594*/ float AnimaCable_Field_9_0_1_33978_025;
    /*0x598*/ float AnimaCable_Field_9_0_1_33978_026;
#if version >= 8
    /*0x59C*/ float AnimaCable_Field_9_0_1_34972_034;
#endif
#if version >= 7
    /*0x5A0*/ uint32_t AnimaCable_Field_9_0_1_34199_033;
    /*0x5A4*/ uint32_t unk_5A4;
    /*0x5A8*/ uint32_t unk_5A8;
#endif

    /*0x5AC*/ CAaBox bounding_box;
#if version >= 8
    /*0x5C4*/ uint32_t unk_5C4;
#endif
    /*0x5C8*/
  };

  struct Node {
    /*0x000*/ C3Vector position;
    /*0x00C*/ C3Vector normal;
    /*0x018*/
  };
  struct Segment {
    /*0x000*/ C3Vector unk_000;
    /*0x00C*/ C3Vector unk_00C;
    /*0x018*/ uint32_t unk_018;
    /*0x01C*/
  };

  /*0x000*/ uint32_t uid;        // globally unique, is a CMapObj
  /*0x004*/ Data data;
  /*0x5CC*/ uint32_t numNodes;   // >= 6
  /*0x5D0*/ Node nodes[numNodes];
#if version >= 7
  /*     */ Segment segments[numNodes - 3];
#endif
  /*     */
};

/*0x000*/ uint32_t version;     // 8
/*0x004*/ uint32_t count;
/*0x008*/ Anima animas[count];
/*     */

_occ, _lgt

This section only applies to versions ≥ WoD.

WoD added _occ.wdt (occlusion) and _lgt.wdt (lights) for each .wdt. They are only used for adt-maps, not WMO-only ones.

occ

MAOI and MAOH might be zero size. (WMO-only WDTs)

MVER

struct
{
  uint32_t version; // 18, just as all others
} mver;

MAOI

struct
{
  uint16_t tile_x; // MAOH entries are per ADT tile
  uint16_t tile_y;
  uint32_t offset; // in MAOH
  uint32_t size;   // always (17*17+16*16)*2
} maoi[];

MAOH

Defines a heightmap that occludes everything behind it. Same content as WDL#MARE_chunks.

short interleaved_map[17*17+16*16];

lgt

Might only have MVER for WMO-only WDTs. Level designers are able to freely place lights, without placing models containing lights now. This is used below lamp posts and alike. As of Legion, there is support for point and spot lights.

MVER

struct
{
  uint32_t version; // ≤ Legion (7.0.1.20740): 18 just as all others, ≥ Legion (7.0.1.20914): 20
} mver;

MPLT

This section only applies to versions ≤ WoD.
struct
{
  uint32 id;
  uint16 tile_x;
  uint16 tile_y;
  CArgb color;
  C3Vector position;
  float unknown[3]; // intensity, and stuff. flicker?
} map_point_lights[];
  • starting some Legion build, these are no longer read for backwards compatibility but MPL2 is required.

MPL2 (Legion+)

This section only applies to versions ≥ Legion.
  • appears to be either, not both MPLT and MPL2, or they need to have the same size.
struct 
{
/*0x00*/  uint32 id;
/*0x04*/  CArgb color;
/*0x08*/  C3Vector position;
/*0x14*/  float unknown[0x6]; //First 3 match MPLT's unknown
/*0x2C*/  uint16 tile_x;
/*0x2E*/  uint16 tile_y;
/*0x30*/  char unknown_2[0x4];
/*0x34*/
} map_point_lights[];
  • the only file I know having this (e3148cc88c7f2fcaebe99c53e5e5079e) has a size of 0x40 for MPL2, which does not match to what the client parses (0x34) --Schlumpf (talk) 03:18, 22 November 2015 (UTC)
  • unknown_2, as an int16 array, has seen values of [0,-1] and [-1,-1] --Barncastle

MPL3 (Shadowlands+)

This section only applies to versions ≥ SL (9.0.1.34490).

TODO

MSLT (Legion+)

This section only applies to versions ≥ Legion.
struct
{
     uint32 id;
     CArgb color;
     C3Vector position;
     float rangeAttenuationStart;// When to start the attenuation of the light, must be <= rangeAttenuationEnd or glitches
     float rangeAttenuationEnd;
     float intensity;
     C3Vector rotation; // radians
     float falloffExponent;
     float innerRadius;
     float outerRadius; // radians
     uint16_t tile_x;
     uint16_t tile_y;
     int16_t unk[2]; // -1 mostly, may be an MLTA ID though.
} map_spot_lights[];

MTEX (Legion+)

This section only applies to versions ≥ Legion.
uint32_t textureFileDataIds[];

MLTA

This section only applies to versions ≥ Legion.
struct {
  float _1;
  float _2;
  uint32_t _3; //Always 1 or 2 ??
} map_lta[];

_fogs

This section only applies to versions ≥ Legion (7.2.5.24076).

Legion added _fogs.wdt for a subset of .wdts. They seem to be only present for terrain maps, not WMO maps. As of Legion (7.2.5.24076) (when they were added) and Legion (7.3.2.25383) (when this paragraph was added), they are all empty and not even read by the client. It is likely that they are merged into the branch by accident and are a Battle feature. The first files with content are zandalar and kultiras with Battle (8.0.1.25902).

MVER

uint32_t version;   // ≥ Legion (7.2.5.24076): 1

VFOG

struct {
/*0x00*/  C3Vector color;           // r g b fog color. 1 equals 255
/*0x0C*/  float intensity[3];       // fog radius related intensity
/*0x18*/  char _unk18[4];
/*0x1C*/  C3Vector position;        // server position
/*0x28*/  float _unk28;             // set to 1.0 on loading
/*0x2C*/  C4Vector rotation;        // quat
/*0x3C*/  float radius[3];          // fog start radius
/*0x48*/  char _unk48[0x14];        // fog level related, as uint32_t mostly 10000, 20000, 50000, 60000, 1
/*0x5C*/  uint32_t modelFileDataId; // the client only supports models with one M2Batch
/*0x60*/  char _unk60[4];           // mostly 0 filled
/*0x64*/  uint32_t id;              // globally unique in the files
/*0x68*/
} volumetric_fogs[];

_mpv

This section only applies to versions ≥ Battle (8.0.1.26287).

As of ≥ Battle (8.0.1.26287) references to _mpv.wdt (particulate volume) have been seen in the client. While these files haven't been shipped (26310), the CMap::Load function does attempt to read them when present. These files were first shipped in Battle (8.0.1.26433).

While the file is chunked, it does require the exact order of #PVPD, #PVMI, #PVBD: #PVMI might override #PVPD, and as soon as #PVBD is read, it is finalised.

MVER

enum mpv_version : uint32_t {
  mpv_version_0,               // ignores the rest of the file (actually, all < 1, so probably just ≥1 as requirement)
  mpv_version_1,               // Battle (8.0.1.26433)
  mpv_version_2,               // Battle (8.0.1.26476) … Battle (8.0.1.26557)
  mpv_version_3,               // Battle (8.0.1.26567) … Battle (8.0.1.27404)
  mpv_version_4,               // ≥ Battle (8.0.1.27481)
};
mpv_version version;

PVPD

struct {
  C2Vector _unk00; // [-1.f, 1.f]
  float _unk08;     // only seen: -0.f
  float _unk0c;
} particle_volume_pd[];

PVMI

If #PVPD was already read, it is nulled out. Note that the inverse is not true, i.e. if #PVMI comes first, #PVPD may be non-null. This is not a bug but actual files have #PVMI first, followed by #PVPD and #PVBD.

struct {
#if version == mpv_version_1
  char _unk00[0xF5C];         // appears to be a huge blob, 0xF5C bytes, including five (binary) WWFParticulateGroups
                              // this might not actually be pure binary WWFParticulateGroups (or that size changed without MVER change), since the block is 0xF84 in Battle (8.0.1.26433)
#else if version == mpv_version_2
  char _unk00[0xFE8]; 
#else if version >= mpv_version_3
  char _unk00[0x10D8]; 
#endif
} particle_volume_mi[];

PVBD

struct {
  uint32_t num_unk1C;
  CAaBox _unk04;         // bounds/extents
  uint32_t _unk1C[8];     // indices into #PVPD
  uint32_t _unk3C;        // boolean: This entry is complete. If false, it is joined with the next entry. It will have the same bounds.
} particle_volume_bd[];