#include "niffile.hpp" #include #include #include #include #include #include #include #include #include "controller.hpp" #include "data.hpp" #include "effect.hpp" #include "exception.hpp" #include "extra.hpp" #include "node.hpp" #include "particle.hpp" #include "physics.hpp" #include "property.hpp" #include "texture.hpp" namespace Nif { Reader::Reader(NIFFile& file, const ToUTF8::StatelessUtf8Encoder* encoder) : mVersion(file.mVersion) , mUserVersion(file.mUserVersion) , mBethVersion(file.mBethVersion) , mFilename(file.mPath) , mHash(file.mHash) , mRecords(file.mRecords) , mRoots(file.mRoots) , mUseSkinning(file.mUseSkinning) , mEncoder(encoder) { } template static std::unique_ptr construct() { auto result = std::make_unique(); result->recType = recordType; return result; } using CreateRecord = std::unique_ptr (*)(); /// These are all the record types we know how to read. static std::map makeFactory() { return { // 4.0.0.2 refers to Bethesda variant of NetImmerse 4.0.0.2 file format // Gamebryo refers to files newer than 4.0.0.2 // Bethesda refers to custom records Bethesda introduced post-4.0.0.2 // NODES // NiNode-like nodes, 4.0.0.2 { "NiNode", &construct }, { "AvoidNode", &construct }, { "NiBillboardNode", &construct }, { "NiBSAnimationNode", &construct }, { "NiBSParticleNode", &construct }, { "NiCollisionSwitch", &construct }, { "NiSortAdjustNode", &construct }, { "RootCollisionNode", &construct }, // NiNode-like nodes, Bethesda { "BSBlastNode", &construct }, { "BSDamageStage", &construct }, { "BSDebrisNode", &construct }, { "BSDistantObjectInstancedNode", &construct }, { "BSFadeNode", &construct }, { "BSLeafAnimNode", &construct }, { "BSMasterParticleSystem", &construct }, { "BSMultiBoundNode", &construct }, { "BSOrderedNode", &construct }, { "BSRangeNode", &construct }, { "BSTreeNode", &construct }, { "BSValueNode", &construct }, // Switch nodes, 4.0.0.2 { "NiSwitchNode", &construct }, { "NiFltAnimationNode", &construct }, { "NiLODNode", &construct }, // NiSequence nodes, 4.0.0.2 { "NiSequenceStreamHelper", &construct }, // NiSequence nodes, Gamebryo { "NiSequence", &construct }, { "NiControllerSequence", &construct }, // Other nodes, 4.0.0.2 { "NiCamera", &construct }, // ACCUMULATORS // 4.0.0.2 { "NiAlphaAccumulator", &construct }, { "NiClusterAccumulator", &construct }, // CONTROLLERS // 4.0.0.2 { "NiAlphaController", &construct }, { "NiBSPArrayController", &construct }, { "NiFlipController", &construct }, { "NiGeomMorpherController", &construct }, { "NiKeyframeController", &construct }, { "NiLookAtController", &construct }, // FIXME: NiLightColorController should have its own struct { "NiLightColorController", &construct }, { "NiMaterialColorController", &construct }, { "NiPathController", &construct }, { "NiRollController", &construct }, { "NiUVController", &construct }, { "NiVisController", &construct }, // Gamebryo { "NiBoneLODController", &construct }, { "NiControllerManager", &construct }, { "NiLightDimmerController", &construct }, { "NiTransformController", &construct }, { "NiTextureTransformController", &construct }, { "NiMultiTargetTransformController", &construct }, // Extra data controllers, Gamebryo { "NiColorExtraDataController", &construct }, { "NiFloatExtraDataController", &construct }, { "NiFloatsExtraDataController", &construct }, { "NiFloatsExtraDataPoint3Controller", &construct }, // Bethesda { "BSFrustumFOVController", &construct }, { "BSKeyframeController", &construct }, { "BSLagBoneController", &construct }, { "BSProceduralLightningController", &construct }, { "BSMaterialEmittanceMultController", &construct }, { "BSNiAlphaPropertyTestRefController", &construct }, { "BSRefractionFirePeriodController", &construct }, { "BSRefractionStrengthController", &construct }, { "BSEffectShaderPropertyColorController", &construct }, { "BSEffectShaderPropertyFloatController", &construct }, { "BSLightingShaderPropertyColorController", &construct }, { "BSLightingShaderPropertyFloatController", &construct }, { "BSLightingShaderPropertyUShortController", &construct }, { "bhkBlendController", &construct }, { "NiBSBoneLODController", &construct }, { "NiLightRadiusController", &construct }, // Interpolators, Gamebryo { "NiBSplineCompFloatInterpolator", &construct }, { "NiBSplineCompPoint3Interpolator", &construct }, { "NiBSplineCompTransformInterpolator", &construct }, { "NiBSplineTransformInterpolator", &construct }, { "NiBlendBoolInterpolator", &construct }, { "NiBlendFloatInterpolator", &construct }, { "NiBlendPoint3Interpolator", &construct }, { "NiBlendTransformInterpolator", &construct }, { "NiBoolInterpolator", &construct }, { "NiBoolTimelineInterpolator", &construct }, { "NiColorInterpolator", &construct }, { "NiFloatInterpolator", &construct }, { "NiLookAtInterpolator", &construct }, { "NiPathInterpolator", &construct }, { "NiPoint3Interpolator", &construct }, { "NiTransformInterpolator", &construct }, // Interpolators, Bethesda { "BSRotAccumTransfInterpolator", &construct }, { "BSTreadTransfInterpolator", &construct }, // DATA // 4.0.0.2 { "NiColorData", &construct }, { "NiFloatData", &construct }, { "NiKeyframeData", &construct }, { "NiMorphData", &construct }, { "NiPalette", &construct }, { "NiPixelData", &construct }, { "NiPosData", &construct }, { "NiSourceTexture", &construct }, { "NiUVData", &construct }, { "NiVisData", &construct }, // Gamebryo { "NiAdditionalGeometryData", &construct }, { "NiBoolData", &construct }, { "NiBSplineData", &construct }, { "NiBSplineBasisData", &construct }, { "NiDefaultAVObjectPalette", &construct }, { "NiTransformData", &construct }, // Bethesda { "BSAnimNote", &construct }, { "BSAnimNotes", &construct }, { "BSPackedAdditionalGeometryData", &construct }, { "BSShaderTextureSet", &construct }, // DYNAMIC EFFECTS // 4.0.0.2 { "NiAmbientLight", &construct }, { "NiDirectionalLight", &construct }, { "NiPointLight", &construct }, { "NiSpotLight", &construct }, { "NiTextureEffect", &construct }, // EXTRA DATA // 4.0.0.2 { "NiExtraData", &construct }, { "NiStringExtraData", &construct }, { "NiTextKeyExtraData", &construct }, { "NiVertWeightsExtraData", &construct }, // Gamebryo { "NiBinaryExtraData", &construct }, { "NiBooleanExtraData", &construct }, { "NiColorExtraData", &construct }, { "NiFloatExtraData", &construct }, { "NiFloatsExtraData", &construct }, { "NiIntegerExtraData", &construct }, { "NiIntegersExtraData", &construct }, { "NiVectorExtraData", &construct }, { "NiStringsExtraData", &construct }, { "NiStringPalette", &construct }, // Bethesda bounds { "BSBound", &construct }, { "BSMultiBound", &construct }, { "BSMultiBoundAABB", &construct }, { "BSMultiBoundOBB", &construct }, { "BSMultiBoundSphere", &construct }, // Bethesda markers { "BSFurnitureMarker", &construct }, { "BSFurnitureMarkerNode", &construct }, { "BSInvMarker", &construct }, // Other Bethesda records { "BSExtraData", &construct }, { "BSBehaviorGraphExtraData", &construct }, { "BSBoneLODExtraData", &construct }, { "BSClothExtraData", &construct }, { "BSCollisionQueryProxyExtraData", &construct }, { "BSConnectPoint::Children", &construct }, { "BSConnectPoint::Parents", &construct }, { "BSDecalPlacementVectorExtraData", &construct }, { "BSDistantObjectExtraData", &construct }, { "BSDistantObjectLargeRefExtraData", &construct }, { "BSEyeCenterExtraData", &construct }, { "BSPackedCombinedSharedGeomDataExtra", &construct }, { "BSPositionData", &construct }, { "BSWArray", &construct }, { "BSXFlags", &construct }, // GEOMETRY // 4.0.0.2 { "NiLines", &construct }, { "NiLinesData", &construct }, { "NiSkinData", &construct }, { "NiSkinInstance", &construct }, { "NiSkinPartition", &construct }, { "NiTriShape", &construct }, { "NiTriShapeData", &construct }, { "NiTriStrips", &construct }, { "NiTriStripsData", &construct }, // Bethesda { "BSDismemberSkinInstance", &construct }, { "BSSkin::Instance", &construct }, { "BSSkin::BoneData", &construct }, { "BSTriShape", &construct }, { "BSDynamicTriShape", &construct }, { "BSLODTriShape", &construct }, { "BSMeshLODTriShape", &construct }, { "BSSegmentedTriShape", &construct }, { "BSSubIndexTriShape", &construct }, // PARTICLES // Geometry, 4.0.0.2 { "NiAutoNormalParticles", &construct }, { "NiAutoNormalParticlesData", &construct }, { "NiParticles", &construct }, { "NiParticlesData", &construct }, { "NiRotatingParticles", &construct }, { "NiRotatingParticlesData", &construct }, // Geometry, Gamebryo { "NiParticleSystem", &construct }, { "NiMeshParticleSystem", &construct }, { "NiPSysData", &construct }, { "NiMeshPSysData", &construct }, // Geometry, Bethesda { "BSStripParticleSystem", &construct }, { "BSStripPSysData", &construct }, // Modifiers, 4.0.0.2 { "NiGravity", &construct }, { "NiParticleBomb", &construct }, { "NiParticleColorModifier", &construct }, { "NiParticleGrowFade", &construct }, { "NiParticleRotation", &construct }, // Modifiers, Gamebryo { "NiPSysAgeDeathModifier", &construct }, { "NiPSysBombModifier", &construct }, { "NiPSysBoundUpdateModifier", &construct }, { "NiPSysColorModifier", &construct }, { "NiPSysDragModifier", &construct }, { "NiPSysGravityModifier", &construct }, { "NiPSysGrowFadeModifier", &construct }, { "NiPSysPositionModifier", &construct }, { "NiPSysRotationModifier", &construct }, { "NiPSysSpawnModifier", &construct }, { "NiPSysMeshUpdateModifier", &construct }, // Modifiers, Bethesda { "BSParentVelocityModifier", &construct }, { "BSPSysHavokUpdateModifier", &construct }, { "BSPSysInheritVelocityModifier", &construct }, { "BSPSysLODModifier", &construct }, { "BSPSysRecycleBoundModifier", &construct }, { "BSPSysScaleModifier", &construct }, { "BSPSysSimpleColorModifier", &construct }, { "BSPSysStripUpdateModifier", &construct }, { "BSPSysSubTexModifier", &construct }, { "BSWindModifier", &construct }, // Emitters, Gamebryo { "NiPSysBoxEmitter", &construct }, { "NiPSysCylinderEmitter", &construct }, { "NiPSysMeshEmitter", &construct }, { "NiPSysSphereEmitter", &construct }, // Emitters, Bethesda { "BSPSysArrayEmitter", &construct }, // Modifier controllers, Gamebryo { "NiPSysAirFieldAirFrictionCtlr", &construct }, { "NiPSysAirFieldInheritVelocityCtlr", &construct }, { "NiPSysAirFieldSpreadCtlr", &construct }, { "NiPSysEmitterCtlr", &construct }, { "NiPSysEmitterDeclinationCtlr", &construct }, { "NiPSysEmitterDeclinationVarCtlr", &construct }, { "NiPSysEmitterInitialRadiusCtlr", &construct }, { "NiPSysEmitterLifeSpanCtlr", &construct }, { "NiPSysEmitterPlanarAngleCtlr", &construct }, { "NiPSysEmitterPlanarAngleVarCtlr", &construct }, { "NiPSysEmitterSpeedCtlr", &construct }, { "NiPSysFieldAttenuationCtlr", &construct }, { "NiPSysFieldMagnitudeCtlr", &construct }, { "NiPSysFieldMaxDistanceCtlr", &construct }, { "NiPSysGravityStrengthCtlr", &construct }, { "NiPSysInitialRotSpeedCtlr", &construct }, { "NiPSysInitialRotSpeedVarCtlr", &construct }, { "NiPSysInitialRotAngleCtlr", &construct }, { "NiPSysInitialRotAngleVarCtlr", &construct }, { "NiPSysModifierActiveCtlr", &construct }, { "NiPSysRotDampeningCtlr", &construct }, // Modifier controllers, Bethesda { "BSPSysMultiTargetEmitterCtlr", &construct }, // Modifier controller data, Gamebryo { "NiPSysEmitterCtlrData", &construct }, // Colliders, 4.0.0.2 { "NiPlanarCollider", &construct }, { "NiSphericalCollider", &construct }, // Colliders, Gamebryo { "NiPSysColliderManager", &construct }, { "NiPSysPlanarCollider", &construct }, { "NiPSysSphericalCollider", &construct }, // Particle system controllers, 4.0.0.2 { "NiParticleSystemController", &construct }, // Particle system controllers, Gamebryo { "NiPSysResetOnLoopCtlr", &construct }, { "NiPSysUpdateCtlr", &construct }, // PHYSICS // Collision objects, Gamebryo { "NiCollisionObject", &construct }, // Collision objects, Bethesda { "bhkCollisionObject", &construct }, { "bhkPCollisionObject", &construct }, { "bhkSPCollisionObject", &construct }, { "bhkNPCollisionObject", &construct }, { "bhkBlendCollisionObject", &construct }, // Constraint records, Bethesda { "bhkBallAndSocketConstraint", &construct }, { "bhkBallSocketConstraintChain", &construct }, { "bhkHingeConstraint", &construct }, { "bhkLimitedHingeConstraint", &construct }, { "bhkRagdollConstraint", &construct }, { "bhkStiffSpringConstraint", &construct }, { "bhkPrismaticConstraint", &construct }, { "bhkMalleableConstraint", &construct }, { "bhkBreakableConstraint", &construct }, // Physics body records, Bethesda { "bhkRigidBody", &construct }, { "bhkRigidBodyT", &construct }, // Physics geometry records, Bethesda { "bhkBoxShape", &construct }, { "bhkCapsuleShape", &construct }, { "bhkCylinderShape", &construct }, { "bhkCompressedMeshShape", &construct }, { "bhkCompressedMeshShapeData", &construct }, { "bhkConvexListShape", &construct }, { "bhkConvexSweepShape", &construct }, { "bhkConvexTransformShape", &construct }, { "bhkConvexVerticesShape", &construct }, { "bhkListShape", &construct }, { "bhkMoppBvTreeShape", &construct }, { "bhkMeshShape", &construct }, { "bhkMultiSphereShape", &construct }, { "bhkNiTriStripsShape", &construct }, { "bhkPackedNiTriStripsShape", &construct }, { "hkPackedNiTriStripsData", &construct }, { "bhkPlaneShape", &construct }, { "bhkSphereShape", &construct }, { "bhkTransformShape", &construct }, // Phantom records, Bethesda { "bhkAabbPhantom", &construct }, { "bhkSimpleShapePhantom", &construct }, // Physics system records, Bethesda { "bhkPhysicsSystem", &construct }, { "bhkRagdollSystem", &construct }, // Action records { "bhkLiquidAction", &construct }, { "bhkOrientHingedBodyAction", &construct }, // Ragdoll template records { "bhkRagdollTemplate", &construct }, { "bhkRagdollTemplateData", &construct }, // Other records { "bhkPoseArray", &construct }, // PROPERTIES // 4.0.0.2 { "NiAlphaProperty", &construct }, { "NiDitherProperty", &construct }, { "NiFogProperty", &construct }, { "NiMaterialProperty", &construct }, { "NiShadeProperty", &construct }, { "NiSpecularProperty", &construct }, { "NiStencilProperty", &construct }, { "NiTexturingProperty", &construct }, { "NiVertexColorProperty", &construct }, { "NiWireframeProperty", &construct }, { "NiZBufferProperty", &construct }, // Shader properties, Bethesda { "BSShaderProperty", &construct }, { "BSShaderPPLightingProperty", &construct }, { "BSShaderNoLightingProperty", &construct }, { "BSDistantTreeShaderProperty", &construct }, { "BSLightingShaderProperty", &construct }, { "BSEffectShaderProperty", &construct }, { "BSSkyShaderProperty", &construct }, { "BSWaterShaderProperty", &construct }, { "DistantLODShaderProperty", &construct }, { "HairShaderProperty", &construct }, { "Lighting30ShaderProperty", &construct }, { "SkyShaderProperty", &construct }, { "TallGrassShaderProperty", &construct }, { "TileShaderProperty", &construct }, { "VolumetricFogShaderProperty", &construct }, { "WaterShaderProperty", &construct }, }; } /// Make the factory map used for parsing the file static const std::map factories = makeFactory(); std::string Reader::versionToString(std::uint32_t version) { int major = (version >> 24) & 0xFF; int minor = (version >> 16) & 0xFF; int patch = (version >> 8) & 0xFF; int rev = version & 0xFF; std::stringstream stream; stream << major << "." << minor << "." << patch << "." << rev; return stream.str(); } void Reader::parse(Files::IStreamPtr&& stream) { const bool writeDebug = sWriteNifDebugLog; if (writeDebug) Log(Debug::Verbose) << "NIF Debug: Reading file: '" << mFilename << "'"; const std::array fileHash = Files::getHash(mFilename, *stream); mHash.append(reinterpret_cast(fileHash.data()), fileHash.size() * sizeof(std::uint64_t)); NIFStream nif(*this, std::move(stream), mEncoder); // Check the header string std::string head = nif.getVersionString(); static const std::array verStrings = { "NetImmerse File Format", "Gamebryo File Format", }; const bool supportedHeader = std::any_of(verStrings.begin(), verStrings.end(), [&](const std::string& verString) { return head.starts_with(verString); }); if (!supportedHeader) throw Nif::Exception("Invalid NIF header: " + head, mFilename); // Get BCD version nif.read(mVersion); // 4.0.0.0 is an older, practically identical version of the format. // It's not used by Morrowind assets but Morrowind supports it. static const std::array supportedVers = { NIFStream::generateVersion(4, 0, 0, 0), NIFFile::VER_MW, }; const bool supportedVersion = std::find(supportedVers.begin(), supportedVers.end(), mVersion) != supportedVers.end(); if (!supportedVersion && !sLoadUnsupportedFiles) throw Nif::Exception("Unsupported NIF version: " + versionToString(mVersion), mFilename); const bool hasEndianness = mVersion >= NIFStream::generateVersion(20, 0, 0, 4); const bool hasUserVersion = mVersion >= NIFStream::generateVersion(10, 0, 1, 8); const bool hasRecTypeListings = mVersion >= NIFStream::generateVersion(5, 0, 0, 1); const bool hasRecTypeHashes = mVersion == NIFStream::generateVersion(20, 3, 1, 2); const bool hasRecordSizes = mVersion >= NIFStream::generateVersion(20, 2, 0, 5); const bool hasGroups = mVersion >= NIFStream::generateVersion(5, 0, 0, 6); const bool hasStringTable = mVersion >= NIFStream::generateVersion(20, 1, 0, 1); const bool hasRecordSeparators = mVersion >= NIFStream::generateVersion(10, 0, 0, 0) && mVersion < NIFStream::generateVersion(10, 2, 0, 0); // Record type list std::vector recTypes; // Record type mapping for each record std::vector recTypeIndices; { std::uint8_t endianness = 1; if (hasEndianness) nif.read(endianness); // TODO: find some big-endian files and investigate the difference if (endianness == 0) throw Nif::Exception("Big endian NIF files are unsupported", mFilename); } if (hasUserVersion) nif.read(mUserVersion); mRecords.resize(nif.get()); // Bethesda stream header { bool hasBSStreamHeader = false; if (mVersion == NIFFile::VER_OB_OLD) hasBSStreamHeader = true; else if (mUserVersion >= 3 && mVersion >= NIFStream::generateVersion(10, 1, 0, 0)) { if (mVersion <= NIFFile::VER_OB || mVersion == NIFFile::VER_BGS) hasBSStreamHeader = mUserVersion <= 11 || mVersion >= NIFFile::VER_OB; } if (hasBSStreamHeader) { nif.read(mBethVersion); nif.getExportString(); // Author if (mBethVersion >= 131) nif.get(); // Unknown else nif.getExportString(); // Process script nif.getExportString(); // Export script if (mBethVersion >= 103) nif.getExportString(); // Max file path } } if (writeDebug) { std::stringstream versionInfo; versionInfo << "NIF Debug: Version: " << versionToString(mVersion); if (mUserVersion) versionInfo << "\nUser version: " << mUserVersion; if (mBethVersion) versionInfo << "\nBSStream version: " << mBethVersion; Log(Debug::Verbose) << versionInfo.str(); } if (hasRecTypeListings) { // TODO: 20.3.1.2 uses DJB hashes instead of strings if (hasRecTypeHashes) throw Nif::Exception("Hashed record types are unsupported", mFilename); else { nif.getSizedStrings(recTypes, nif.get()); nif.readVector(recTypeIndices, mRecords.size()); } } if (hasRecordSizes) // Record sizes { std::vector recSizes; // Currently unused nif.readVector(recSizes, mRecords.size()); } if (hasStringTable) { std::uint32_t stringNum, maxStringLength; nif.read(stringNum); nif.read(maxStringLength); nif.getSizedStrings(mStrings, stringNum); } if (hasGroups) { std::vector groups; // Currently unused nif.readVector(groups, nif.get()); } for (std::size_t i = 0; i < mRecords.size(); i++) { std::unique_ptr r; std::string rec = hasRecTypeListings ? recTypes[recTypeIndices[i]] : nif.get(); if (rec.empty()) { std::stringstream error; error << "Record type is blank (index " << i << ")"; throw Nif::Exception(error.str(), mFilename); } // Record separator. Some Havok records in Oblivion do not have it. if (hasRecordSeparators && !rec.starts_with("bhk") && nif.get()) throw Nif::Exception("Non-zero separator precedes " + rec + ", index " + std::to_string(i), mFilename); const auto entry = factories.find(rec); if (entry == factories.end()) throw Nif::Exception("Unknown record type " + rec, mFilename); r = entry->second(); if (writeDebug) Log(Debug::Verbose) << "NIF Debug: Reading record of type " << rec << ", index " << i; assert(r != nullptr); assert(r->recType != RC_MISSING); r->recName = std::move(rec); r->recIndex = i; r->read(&nif); mRecords[i] = std::move(r); } // Determine which records are roots mRoots.resize(nif.get()); for (std::size_t i = 0; i < mRoots.size(); i++) { std::int32_t idx; nif.read(idx); if (idx >= 0 && static_cast(idx) < mRecords.size()) { mRoots[i] = mRecords[idx].get(); } else { mRoots[i] = nullptr; Log(Debug::Warning) << "NIFFile Warning: Root " << i + 1 << " does not point to a record: index " << idx << ". File: " << mFilename; } } // Once parsing is done, do post-processing. for (const auto& record : mRecords) record->post(*this); } void Reader::setUseSkinning(bool skinning) { mUseSkinning = skinning; } std::atomic_bool Reader::sLoadUnsupportedFiles = false; std::atomic_bool Reader::sWriteNifDebugLog = false; void Reader::setLoadUnsupportedFiles(bool load) { sLoadUnsupportedFiles = load; } void Reader::setWriteNifDebugLog(bool value) { sWriteNifDebugLog = value; } std::string Reader::getString(std::uint32_t index) const { if (index == std::numeric_limits::max()) return std::string(); return mStrings.at(index); } }