whiteoutlib 0.2.0

Read and write Blizzard game assets from Rust: models (MDX, M2, M3), textures (BLP, DDS, PNG, JPEG, BMP, TGA, TIFF, GIF) and archives (CASC, MPQ).
Documentation
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2026 Fernando Sahmkow

void BinaryWriterVisitor::visit(const ConvexHullHalfEdge& edge, u32 version) {
    (void)version;
    writer.write(edge.type);
    writer.write(edge.faceIndex);
    writer.write(edge.vertexIndex);
    writer.write(edge.nextAroundVertex);
}

void BinaryWriterVisitor::visit(const PhysicsMeshBvhNode& normal, u32 version) {
    if (version >= 1) {
        writer.write(normal.octahedral.octX);
        writer.write(normal.octahedral.octY);
        writer.write(normal.octahedral.slabMin);
        writer.write(normal.octahedral.slabMax);
    } else {
        writer.write(normal.normal);
    }
}

void BinaryWriterVisitor::visit(const PhysicsMeshTriangle& triangle, u32 version) {
    (void)version;
    writer.write(triangle.vertexIndex0);
    writer.write(triangle.vertexIndex1);
    writer.write(triangle.vertexIndex2);
    writer.write(triangle.edgeIndex0);
    writer.write(triangle.edgeIndex1);
    writer.write(triangle.edgeIndex2);
    writer.write(triangle.reserved);
    writer.write(triangle.flags);
}

void BinaryWriterVisitor::visit(const PhysicsMeshEdge& edge, u32 version) {
    (void)version;
    writer.write(edge.edgeType);
    writer.write(edge.vertexA);
    writer.write(edge.vertexB);
    writer.write(edge.faceA);
    writer.write(edge.faceB);
}

void BinaryWriterVisitor::visit(const PhysicsShape& shape, u32 version) {
    writer.write(shape.transform);
    if (version <= 1) {
        writer.write(shape.collisionMargin);
        writer.write(shape.shapeType);
        writer.write<u8>(0);
        writer.write<u8>(0);
        writer.write<u8>(0);
        auto& legacyVertices = legacyVec3Scratch.emplace_back();
        const auto& positions = shape.shapeType == PhysicsShapeType::Mesh
                                    ? shape.meshVertexPositions
                                    : shape.hullVertexPositions;
        legacyVertices.reserve(positions.size());
        for (const auto& v : positions) {
            legacyVertices.push_back(Vector3f{v.x, v.y, v.z});
        }
        visit(legacyVertices);
        visit(shape.deprecated.v1.unknown0);
        visit(shape.deprecated.v1.faceIndices);
        visit(shape.deprecated.v1.planeEquations);
        writer.write(shape.shapeDimensions); // v1 halfExtents
        return;
    }

    // Version 2+
    writer.write(shape.shapeType);
    writer.write<u8>(0);
    writer.write<u8>(0);
    writer.write<u8>(0);
    writer.write(shape.oldSizes);

    writer.write(shape.reserved0);
    writer.write(shape.shapeDimensions);
    visit(shape.hullFaceNormals);
    visit(shape.hullVertexPositions);
    visit(shape.hullHalfEdges);
    visit(shape.hullVertexFaceIndices);
    writer.write(shape.hullCenter);
    writer.write(shape.hullFaceNormalCount);
    writer.write(shape.hullVertexCount);
    writer.write(shape.hullHalfEdgeCount);
    writer.write(shape.hullUnknown0);
    writer.write(shape.hullUnknown1);

    if (version >= 3) {
        visit(shape.meshBvhNodes);
        visit(shape.meshVertexPositions);
        visit(shape.meshFaceIndices16);
        visit(shape.meshFaceIndices32);
    }
    writer.write(shape.meshBoundsCenter);
    writer.write(shape.meshBoundsExtent);
    writer.write(shape.meshTolerance);
    if (version == 2) {
        visit(shape.meshBvhNodes);
        auto& legacyPositions = legacyVec3Scratch.emplace_back();
        legacyPositions.reserve(shape.meshVertexPositions.size());
        for (const auto& v : shape.meshVertexPositions) {
            legacyPositions.push_back(Vector3f{v.x, v.y, v.z});
        }
        visit(legacyPositions);
        auto& triangles = legacyTriangleScratch.emplace_back();
        triangles.reserve(shape.meshFaceIndices32.size());
        for (const auto& f : shape.meshFaceIndices32) {
            auto& t = triangles.emplace_back();
            t.vertexIndex0 = f[0];
            t.vertexIndex1 = f[1];
            t.vertexIndex2 = f[2];
            t.edgeIndex0 = f[3];
            t.edgeIndex1 = f[4];
            t.edgeIndex2 = f[5];
            t.reserved = static_cast<u16>(f[6] & 0xFFFF);
            t.flags = static_cast<u16>(f[6] >> 16);
            t.setVersion(0);
        }
        visit(triangles);
        visit(shape.deprecated.v2.unknown2);
        // 6-dword v2 tail (292-byte entry); see the parse visitor
        writer.write(shape.deprecated.v2.tailUnknown0);
        writer.write(shape.meshVertexCount);
        writer.write(shape.meshFaceIndex32Count);
        writer.write(shape.deprecated.v2.tailUnknown1);
        writer.write(shape.deprecated.v2.tailUnknown2);
        writer.write(shape.meshTreeDepth);
        return;
    }
    writer.write(shape.meshNormalCount);
    writer.write(shape.meshVertexCount);
    writer.write(shape.meshFaceIndex16Count);
    writer.write(shape.meshFaceIndex32Count);
    writer.write(shape.meshUnknown1);
    writer.write(shape.meshReserved);
    writer.write(shape.meshTreeDepth);
    writer.write(shape.meshCollisionMargin);
}

void BinaryWriterVisitor::visit(const RigidBody& body, u32 version) {
    if (version <= 2) {
        // Legacy Havok-era layout: 80 bytes base + Ref<PHSH> + 12 bytes post-ref
        writer.write(body.density);
        writer.write(body.friction);
        writer.write(body.restitution);
        writer.write(body.linearDamping);
        writer.write(body.angularDamping);
        writer.write(body.gravityScale);
        for (int r = 0; r < 3; ++r)
            for (int c = 0; c < 3; ++c)
                writer.write(body.deprecated.inertiaTensor[r][c]);
        writer.write(body.parentBoneIndex);
        writer.write(body.deprecated.boneIndex);
        writer.write(body.deprecated.reserved);
    } else {
        writer.write(body.simulationType);
        writer.write(body.parentBoneIndex);
        writer.write(body.physicsType);
        writer.write(body.density);
        writer.write(body.friction);
        writer.write(body.restitution);
        writer.write(body.linearDamping);
        writer.write(body.angularDamping);
        writer.write(body.gravityScale);
        if (version >= 4) {
            writer.write(body.dynamicState);
            writer.write(body.dynamicBlendOut);
        }
    }
    visit(body.rigidBodyShape);
    writer.write(body.flags);
    writer.write(body.localForces);
    writer.write(body.worldForces);
    writer.write(body.priority);
}

void BinaryWriterVisitor::visit(const PhysicsConstraint& constraint, u32 version) {
    (void)version;
    visit(constraint.dependents);
    writer.write(constraint.rigidBody1);
    writer.write(constraint.rigidBody2);
    writer.write(constraint.flags);
    writer.write(constraint.breakForce);
}

void BinaryWriterVisitor::visit(const PhysicsJoint& joint, u32 version) {
    (void)version;
    writer.write(joint.jointType);
    writer.write(joint.boneIndex1);
    writer.write(joint.boneIndex2);
    writer.write(joint.matrixBody1);
    writer.write(joint.matrixBody2);
    writer.write(joint.enableLimits);
    writer.write(joint.limitMin);
    writer.write(joint.limitMax);
    writer.write(joint.coneAngle);
    writer.write(joint.enableFriction);
    writer.write(joint.friction);
    writer.write(joint.dampingRatio);
    writer.write(joint.angularFrequency);
    writer.write(joint.breakThreshold);
    writer.write(joint.enableShape);
    writer.write<u8>(0);
    writer.write<u8>(0);
    writer.write<u8>(0);
}

void BinaryWriterVisitor::visit(const ClothPhysics& cloth, u32 version) {
    (void)version;
    writer.write(cloth.clothMeshCount);
    writer.write(cloth.skinBoneCount);
    visit(cloth.skinBones);
    visit(cloth.simEnabled);
    visit(cloth.vertexBones);
    visit(cloth.vertexWeights);
    visit(cloth.colliders);
    visit(cloth.proxies);
    writer.write(cloth.density);
    writer.write(cloth.tracking);
    writer.write(cloth.stretchStiffness);
    writer.write(cloth.horizontalStiffness);
    writer.write(cloth.bendingStiffness);
    writer.write(cloth.damping);
    writer.write(cloth.friction);
    writer.write(cloth.gravity);
    writer.write(cloth.explosionScale);
    writer.write(cloth.windScale);
    writer.write(cloth.shearStiffness);
    writer.write(cloth.dragFactor);
    if (version >= 4) {
        writer.write(cloth.liftFactor);
        writer.write(cloth.sphereStiffness);
        writer.write(cloth.flatten);
        writer.write(cloth.active);
        writer.write(cloth.useSkinCollision);
        writer.write(cloth.skinOffset);
        writer.write(cloth.skinExponent);
        writer.write(cloth.skinStiffness);
        writer.write(cloth.localChannels);
        writer.write(cloth.localWind);
    }
}

void BinaryWriterVisitor::visit(const ClothCollider& collider, u32 version) {
    (void)version;
    writer.write(collider.transform);
    writer.write(collider.radius);
    writer.write(collider.height);
    writer.write(collider.padding);
}

void BinaryWriterVisitor::visit(const ClothProxy& proxy, u32 version) {
    (void)version;
    writer.write(proxy.proxyIndex);
    writer.write(proxy.clothIndex);
    visit(proxy.proxyVertices);
    visit(proxy.proxyWeights);
}