whiteoutlib 0.1.2

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

#include <whiteout/models/m2/parser.h>
#include "../../common/binary_reader.h"
#include "../../common/streams.h"
#include "binary_parse_visitor.h"
#include "chunk_parser.h"
#include "wow_file_system.h"

#include <cstring>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <unordered_map>

namespace whiteout {
namespace m2 {

using common::BinaryReader;

class Parser::Impl {
public:
    std::vector<std::string> issues;
    ChunkParser chunkParser;

    void parse(WoWFileSystem& wfs, Model& result);
    void parseBase(BinaryReader& reader, BaseFile& file, WoWFileSystem* wfs);
    void parseSkin(BinaryReader& reader, SkinFile& skinFile, WoWFileSystem* wfs);

    void reportIssue(const std::string& message);
};

Parser::Parser() : pImpl(std::make_unique<Impl>()) {}

Parser::~Parser() = default;

Model Parser::parse(interfaces::VirtualPathFileSystem& fs, const std::string& filePath) {
    pImpl->issues.clear();

    WoWFileSystem wfs(fs, filePath);
    Model model;
    wfs.exploratorySearch();

    pImpl->parse(wfs, model);

    pImpl->chunkParser.drainIssues(pImpl->issues);
    return model;
}

Model Parser::parse(interfaces::CascFileSystem& cascFs, std::span<const uint8_t> buffer) {
    pImpl->issues.clear();

    WoWFileSystem wfs(cascFs, buffer);
    Model model;

    pImpl->parse(wfs, model);

    pImpl->chunkParser.drainIssues(pImpl->issues);
    return model;
}

const std::vector<std::string>& Parser::getIssues() const {
    return pImpl->issues;
}

bool Parser::hasIssues() const {
    return !pImpl->issues.empty();
}

void Parser::Impl::reportIssue(const std::string& message) {
    issues.push_back(message);
}

void Parser::Impl::parse(WoWFileSystem& wfs, Model& result) {
    BaseFile m2;

    {
        auto m2Data = wfs.getM2Base();
        common::span_streambuf sbuf(m2Data);
        std::istream in(&sbuf);
        BinaryReader reader(in);
        parseBase(reader, m2, &wfs);
    }
    result = std::move(m2.header.model);
    const auto readSkinProfile = [&](std::vector<SkinProfile>& profiles,
                                     std::span<const u8> skinData, size_t /*i*/) {
        common::span_streambuf sbuf(skinData);
        std::istream in(&sbuf);
        BinaryReader reader(in);
        SkinFile skinFile;
        skinFile.version = m2.header.version;
        parseSkin(reader, skinFile, &wfs);
        profiles.emplace_back(std::move(skinFile.profile));
    };

    if (m2.sfid_chunk) {
        const auto& sfid = *m2.sfid_chunk;
        for (u32 i = 0; i < static_cast<u32>(sfid.skinFileDataIds.size()); ++i) {
            auto skinData = wfs.getSkin(i, false);
            if (skinData.empty())
                continue;
            readSkinProfile(result.skinProfiles, skinData, i);
        }
        for (u32 i = 0; i < static_cast<u32>(sfid.lodSkinFileDataIds.size()); ++i) {
            auto skinData = wfs.getSkin(i, true);
            if (skinData.empty())
                continue;
            readSkinProfile(result.lodProfiles, skinData, i);
        }
    }

    std::optional<SkeletonFile> skeleton;
    bool hasParent = false;
    do {
        hasParent = false;
        auto skelData = wfs.getSkeleton();
        if (!skelData.empty()) {
            common::span_streambuf sbuf(skelData);
            std::istream in(&sbuf);
            BinaryReader reader(in);
            skeleton.emplace();
            chunkParser.parseChunkedSkeleton(reader, skeleton.value(), &wfs);
            if (skeleton->skpd_chunk) {
                hasParent = true;
                wfs.setParentSkeletonChunk(*skeleton->skpd_chunk);
                skeleton.reset();
            }
        }
    } while (hasParent);

    if (skeleton) {
        if (skeleton->skpd_chunk) {
            reportIssue("Parent skeleton reference found, but parent skeleton parsing is not yet "
                        "implemented");
        }
        if (skeleton->skb1_chunk) {
            result.bones = std::move(skeleton->skb1_chunk->bones);
            result.keyBoneIds = std::move(skeleton->skb1_chunk->keyBoneLookup);
        }
        if (skeleton->sks1_chunk) {
            result.globalLoops = std::move(skeleton->sks1_chunk->globalLoops);
            result.sequences = std::move(skeleton->sks1_chunk->sequences);
            result.sequenceIdxHashById = std::move(skeleton->sks1_chunk->sequenceLookups);
        }
        if (skeleton->ska1_chunk) {
            result.attachments = std::move(skeleton->ska1_chunk->attachments);
            result.attachmentIndicesById = std::move(skeleton->ska1_chunk->attachmentLookupTable);
        }
    }

    if (m2.expt_chunk) {
        size_t const numParticleEmitters = result.particleEmitters.size();
        for (size_t i = 0; i < numParticleEmitters; ++i) {
            auto& emitter_extension = result.particleEmitters[i].extension;
            emitter_extension.emplace();
            emitter_extension->zSource = m2.expt_chunk->extendedParticles[i].zSource;
            emitter_extension->colorMult = m2.expt_chunk->extendedParticles[i].colorMult;
            emitter_extension->alphaMult = m2.expt_chunk->extendedParticles[i].alphaMult;
        }
    }

    if (m2.exp2_chunk) {
        size_t const numParticleEmitters = result.particleEmitters.size();
        for (size_t i = 0; i < numParticleEmitters; ++i) {
            auto emitter_externsion = m2.exp2_chunk->emitterExtensions[i];
            result.particleEmitters[i].extension = std::move(emitter_externsion);
        }
    }

    if (m2.txid_chunk) {
        result.texture_ids = m2.txid_chunk->textureIds;
    }

    if (m2.ldv1_chunk) {
        result.lodProfile = *m2.ldv1_chunk;
    }

    // Merge remaining chunk data into Model
    if (m2.txac_chunk)
        result.textureCombinerHints = std::move(m2.txac_chunk->entries);
    if (m2.pabc_chunk)
        result.parentSequenceReplacements =
            std::move(m2.pabc_chunk->replacementParentSequenceLookups);
    if (m2.padc_chunk)
        result.parentTextureWeights = std::move(m2.padc_chunk->textureWeights);
    if (m2.psbc_chunk)
        result.parentSequenceBounds = std::move(m2.psbc_chunk->parentSequenceBounds);
    if (m2.pedc_chunk)
        result.parentEventData = std::move(m2.pedc_chunk->parentEventData);
    if (m2.rpid_chunk) {
        result.recursiveParticleModelIds.reserve(m2.rpid_chunk->recursiveParticleModels.size());
        for (const auto& e : m2.rpid_chunk->recursiveParticleModels)
            result.recursiveParticleModelIds.push_back(e.fileDataId);
    }
    if (m2.gpid_chunk) {
        result.geometryParticleModelIds.reserve(m2.gpid_chunk->geometryParticleModels.size());
        for (const auto& e : m2.gpid_chunk->geometryParticleModels)
            result.geometryParticleModelIds.push_back(e.fileDataId);
    }
    if (m2.wfv3_chunk)
        result.waterData = m2.wfv3_chunk->data;
    if (m2.pgd1_chunk)
        result.particleGeosets = std::move(m2.pgd1_chunk->particleGeosetData);
    if (m2.pfdc_chunk)
        result.physicsFileData = std::move(m2.pfdc_chunk->physicsData);
    if (m2.edgf_chunk)
        result.edgeFadeEntries = std::move(m2.edgf_chunk->entries);
    if (m2.nerf_chunk)
        result.nerfEntries = std::move(m2.nerf_chunk->entries);
    if (m2.detl_chunk)
        result.detailedLightEntries = std::move(m2.detl_chunk->records);
    if (m2.dboc_chunk)
        result.debugOcclusionEntries = std::move(m2.dboc_chunk->entries);
    if (m2.afra_chunk)
        result.animFrameData = std::move(m2.afra_chunk->data);
    if (m2.pcol_chunk) {
        PhysicsCollision pc;
        pc.vertexPositions = std::move(m2.pcol_chunk->vertexPositions);
        pc.faceNormals = std::move(m2.pcol_chunk->faceNormals);
        pc.indices = std::move(m2.pcol_chunk->indices);
        pc.flags = std::move(m2.pcol_chunk->flags);
        result.physicsCollision = std::move(pc);
    }
    if (m2.dpiv_chunk)
        result.dpivData = m2.dpiv_chunk->data;
    if (m2.texl_chunk)
        result.texturedLightEntries = std::move(m2.texl_chunk->texturedLights);
}

void Parser::Impl::parseBase(BinaryReader& reader, BaseFile& file, WoWFileSystem* wfs) {
    u32 magic = reader.read<u32>();
    reader.setPosition(0);

    if (magic == MD20_TAG) {
        file.format = Format::ClassicMD20;
        wfs->exploratorySearch();
        BinaryParseVisitor parser(reader, wfs);
        parser.read(file.header);
        return;
    } else if (magic == MD21_TAG) {
        file.format = Format::LegionMD21;
        chunkParser.parseChunkedBase(reader, file, wfs);
        return;
    }
    std::string const error = "Invalid M2 magic: expected MD20 or MD21, got '" +
                              std::string(reinterpret_cast<char*>(&magic), 4) + "'";
    issues.push_back(error);
}

void Parser::Impl::parseSkin(BinaryReader& reader, SkinFile& skinFile, WoWFileSystem* wfs) {
    u32 magic = reader.read<u32>();
    reader.setPosition(0);

    if (magic == SKIN_TAG) {
        skinFile.profile = SkinProfile();
        BinaryParseVisitor parser(reader, wfs);
        parser.setVersion(skinFile.version);
        parser.read(skinFile.profile);
        return;
    }
    std::string const error = "Invalid M2 magic: expected SKIN, got '" +
                              std::string(reinterpret_cast<char*>(&magic), 4) + "'";
    issues.push_back(error);
}

} // namespace m2
} // namespace whiteout