mirror of
				https://github.com/OpenMW/openmw.git
				synced 2025-10-31 07:56:38 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			356 lines
		
	
	
	
		
			21 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			356 lines
		
	
	
	
		
			21 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include "niffile.hpp"
 | |
| #include "effect.hpp"
 | |
| 
 | |
| #include <components/files/hash.hpp>
 | |
| 
 | |
| #include <array>
 | |
| #include <map>
 | |
| #include <sstream>
 | |
| #include <algorithm>
 | |
| 
 | |
| namespace Nif
 | |
| {
 | |
| 
 | |
| /// Open a NIF stream. The name is used for error messages.
 | |
| NIFFile::NIFFile(Files::IStreamPtr&& stream, const std::string &name)
 | |
|     : filename(name)
 | |
| {
 | |
|     parse(std::move(stream));
 | |
| }
 | |
| 
 | |
| template <typename NodeType, RecordType recordType>
 | |
| static std::unique_ptr<Record> construct()
 | |
| {
 | |
|     auto result = std::make_unique<NodeType>();
 | |
|     result->recType = recordType;
 | |
|     return result;
 | |
| }
 | |
| 
 | |
| using CreateRecord = std::unique_ptr<Record> (*)();
 | |
| 
 | |
| ///These are all the record types we know how to read.
 | |
| static std::map<std::string, CreateRecord> makeFactory()
 | |
| {
 | |
|     return 
 | |
|     {
 | |
|         {"NiNode"                       , &construct <NiNode                      , RC_NiNode                     >},
 | |
|         {"NiSwitchNode"                 , &construct <NiSwitchNode                , RC_NiSwitchNode               >},
 | |
|         {"NiLODNode"                    , &construct <NiLODNode                   , RC_NiLODNode                  >},
 | |
|         {"NiFltAnimationNode"           , &construct <NiFltAnimationNode          , RC_NiFltAnimationNode         >},
 | |
|         {"AvoidNode"                    , &construct <NiNode                      , RC_AvoidNode                  >},
 | |
|         {"NiCollisionSwitch"            , &construct <NiNode                      , RC_NiCollisionSwitch          >},
 | |
|         {"NiBSParticleNode"             , &construct <NiNode                      , RC_NiBSParticleNode           >},
 | |
|         {"NiBSAnimationNode"            , &construct <NiNode                      , RC_NiBSAnimationNode          >},
 | |
|         {"NiBillboardNode"              , &construct <NiNode                      , RC_NiBillboardNode            >},
 | |
|         {"NiTriShape"                   , &construct <NiTriShape                  , RC_NiTriShape                 >},
 | |
|         {"NiTriStrips"                  , &construct <NiTriStrips                 , RC_NiTriStrips                >},
 | |
|         {"NiLines"                      , &construct <NiLines                     , RC_NiLines                    >},
 | |
|         {"NiParticles"                  , &construct <NiParticles                 , RC_NiParticles                >},
 | |
|         {"NiRotatingParticles"          , &construct <NiParticles                 , RC_NiParticles                >},
 | |
|         {"NiAutoNormalParticles"        , &construct <NiParticles                 , RC_NiParticles                >},
 | |
|         {"NiCamera"                     , &construct <NiCamera                    , RC_NiCamera                   >},
 | |
|         {"RootCollisionNode"            , &construct <NiNode                      , RC_RootCollisionNode          >},
 | |
|         {"NiTexturingProperty"          , &construct <NiTexturingProperty         , RC_NiTexturingProperty        >},
 | |
|         {"NiFogProperty"                , &construct <NiFogProperty               , RC_NiFogProperty              >},
 | |
|         {"NiMaterialProperty"           , &construct <NiMaterialProperty          , RC_NiMaterialProperty         >},
 | |
|         {"NiZBufferProperty"            , &construct <NiZBufferProperty           , RC_NiZBufferProperty          >},
 | |
|         {"NiAlphaProperty"              , &construct <NiAlphaProperty             , RC_NiAlphaProperty            >},
 | |
|         {"NiVertexColorProperty"        , &construct <NiVertexColorProperty       , RC_NiVertexColorProperty      >},
 | |
|         {"NiShadeProperty"              , &construct <NiShadeProperty             , RC_NiShadeProperty            >},
 | |
|         {"NiDitherProperty"             , &construct <NiDitherProperty            , RC_NiDitherProperty           >},
 | |
|         {"NiWireframeProperty"          , &construct <NiWireframeProperty         , RC_NiWireframeProperty        >},
 | |
|         {"NiSpecularProperty"           , &construct <NiSpecularProperty          , RC_NiSpecularProperty         >},
 | |
|         {"NiStencilProperty"            , &construct <NiStencilProperty           , RC_NiStencilProperty          >},
 | |
|         {"NiVisController"              , &construct <NiVisController             , RC_NiVisController            >},
 | |
|         {"NiGeomMorpherController"      , &construct <NiGeomMorpherController     , RC_NiGeomMorpherController    >},
 | |
|         {"NiKeyframeController"         , &construct <NiKeyframeController        , RC_NiKeyframeController       >},
 | |
|         {"NiAlphaController"            , &construct <NiAlphaController           , RC_NiAlphaController          >},
 | |
|         {"NiRollController"             , &construct <NiRollController            , RC_NiRollController           >},
 | |
|         {"NiUVController"               , &construct <NiUVController              , RC_NiUVController             >},
 | |
|         {"NiPathController"             , &construct <NiPathController            , RC_NiPathController           >},
 | |
|         {"NiMaterialColorController"    , &construct <NiMaterialColorController   , RC_NiMaterialColorController  >},
 | |
|         {"NiBSPArrayController"         , &construct <NiBSPArrayController        , RC_NiBSPArrayController       >},
 | |
|         {"NiParticleSystemController"   , &construct <NiParticleSystemController  , RC_NiParticleSystemController >},
 | |
|         {"NiFlipController"             , &construct <NiFlipController            , RC_NiFlipController           >},
 | |
|         {"NiAmbientLight"               , &construct <NiLight                     , RC_NiLight                    >},
 | |
|         {"NiDirectionalLight"           , &construct <NiLight                     , RC_NiLight                    >},
 | |
|         {"NiPointLight"                 , &construct <NiPointLight                , RC_NiLight                    >},
 | |
|         {"NiSpotLight"                  , &construct <NiSpotLight                 , RC_NiLight                    >},
 | |
|         {"NiTextureEffect"              , &construct <NiTextureEffect             , RC_NiTextureEffect            >},
 | |
|         {"NiExtraData"                  , &construct <NiExtraData                 , RC_NiExtraData                >},
 | |
|         {"NiVertWeightsExtraData"       , &construct <NiVertWeightsExtraData      , RC_NiVertWeightsExtraData     >},
 | |
|         {"NiTextKeyExtraData"           , &construct <NiTextKeyExtraData          , RC_NiTextKeyExtraData         >},
 | |
|         {"NiStringExtraData"            , &construct <NiStringExtraData           , RC_NiStringExtraData          >},
 | |
|         {"NiGravity"                    , &construct <NiGravity                   , RC_NiGravity                  >},
 | |
|         {"NiPlanarCollider"             , &construct <NiPlanarCollider            , RC_NiPlanarCollider           >},
 | |
|         {"NiSphericalCollider"          , &construct <NiSphericalCollider         , RC_NiSphericalCollider        >},
 | |
|         {"NiParticleGrowFade"           , &construct <NiParticleGrowFade          , RC_NiParticleGrowFade         >},
 | |
|         {"NiParticleColorModifier"      , &construct <NiParticleColorModifier     , RC_NiParticleColorModifier    >},
 | |
|         {"NiParticleRotation"           , &construct <NiParticleRotation          , RC_NiParticleRotation         >},
 | |
|         {"NiFloatData"                  , &construct <NiFloatData                 , RC_NiFloatData                >},
 | |
|         {"NiTriShapeData"               , &construct <NiTriShapeData              , RC_NiTriShapeData             >},
 | |
|         {"NiTriStripsData"              , &construct <NiTriStripsData             , RC_NiTriStripsData            >},
 | |
|         {"NiLinesData"                  , &construct <NiLinesData                 , RC_NiLinesData                >},
 | |
|         {"NiVisData"                    , &construct <NiVisData                   , RC_NiVisData                  >},
 | |
|         {"NiColorData"                  , &construct <NiColorData                 , RC_NiColorData                >},
 | |
|         {"NiPixelData"                  , &construct <NiPixelData                 , RC_NiPixelData                >},
 | |
|         {"NiMorphData"                  , &construct <NiMorphData                 , RC_NiMorphData                >},
 | |
|         {"NiKeyframeData"               , &construct <NiKeyframeData              , RC_NiKeyframeData             >},
 | |
|         {"NiSkinData"                   , &construct <NiSkinData                  , RC_NiSkinData                 >},
 | |
|         {"NiUVData"                     , &construct <NiUVData                    , RC_NiUVData                   >},
 | |
|         {"NiPosData"                    , &construct <NiPosData                   , RC_NiPosData                  >},
 | |
|         {"NiParticlesData"              , &construct <NiParticlesData             , RC_NiParticlesData            >},
 | |
|         {"NiRotatingParticlesData"      , &construct <NiRotatingParticlesData     , RC_NiParticlesData            >},
 | |
|         {"NiAutoNormalParticlesData"    , &construct <NiParticlesData             , RC_NiParticlesData            >},
 | |
|         {"NiSequenceStreamHelper"       , &construct <NiSequenceStreamHelper      , RC_NiSequenceStreamHelper     >},
 | |
|         {"NiSourceTexture"              , &construct <NiSourceTexture             , RC_NiSourceTexture            >},
 | |
|         {"NiSkinInstance"               , &construct <NiSkinInstance              , RC_NiSkinInstance             >},
 | |
|         {"NiLookAtController"           , &construct <NiLookAtController          , RC_NiLookAtController         >},
 | |
|         {"NiPalette"                    , &construct <NiPalette                   , RC_NiPalette                  >},
 | |
|         {"NiIntegerExtraData"           , &construct <NiIntegerExtraData          , RC_NiIntegerExtraData         >},
 | |
|         {"NiIntegersExtraData"          , &construct <NiIntegersExtraData         , RC_NiIntegersExtraData        >},
 | |
|         {"NiBinaryExtraData"            , &construct <NiBinaryExtraData           , RC_NiBinaryExtraData          >},
 | |
|         {"NiBooleanExtraData"           , &construct <NiBooleanExtraData          , RC_NiBooleanExtraData         >},
 | |
|         {"NiVectorExtraData"            , &construct <NiVectorExtraData           , RC_NiVectorExtraData          >},
 | |
|         {"NiColorExtraData"             , &construct <NiVectorExtraData           , RC_NiColorExtraData           >},
 | |
|         {"NiFloatExtraData"             , &construct <NiFloatExtraData            , RC_NiFloatExtraData           >},
 | |
|         {"NiFloatsExtraData"            , &construct <NiFloatsExtraData           , RC_NiFloatsExtraData          >},
 | |
|         {"NiStringPalette"              , &construct <NiStringPalette             , RC_NiStringPalette            >},
 | |
|         {"NiBoolData"                   , &construct <NiBoolData                  , RC_NiBoolData                 >},
 | |
|         {"NiSkinPartition"              , &construct <NiSkinPartition             , RC_NiSkinPartition            >},
 | |
|         {"BSXFlags"                     , &construct <NiIntegerExtraData          , RC_BSXFlags                   >},
 | |
|         {"BSBound"                      , &construct <BSBound                     , RC_BSBound                    >},
 | |
|         {"NiTransformData"              , &construct <NiKeyframeData              , RC_NiKeyframeData             >},
 | |
|         {"BSFadeNode"                   , &construct <NiNode                      , RC_NiNode                     >},
 | |
|         {"bhkBlendController"           , &construct <bhkBlendController          , RC_bhkBlendController         >},
 | |
|         {"NiFloatInterpolator"          , &construct <NiFloatInterpolator         , RC_NiFloatInterpolator        >},
 | |
|         {"NiBoolInterpolator"           , &construct <NiBoolInterpolator          , RC_NiBoolInterpolator         >},
 | |
|         {"NiPoint3Interpolator"         , &construct <NiPoint3Interpolator        , RC_NiPoint3Interpolator       >},
 | |
|         {"NiTransformController"        , &construct <NiKeyframeController        , RC_NiKeyframeController       >},
 | |
|         {"NiTransformInterpolator"      , &construct <NiTransformInterpolator     , RC_NiTransformInterpolator    >},
 | |
|         {"NiColorInterpolator"          , &construct <NiColorInterpolator         , RC_NiColorInterpolator        >},
 | |
|         {"BSShaderTextureSet"           , &construct <BSShaderTextureSet          , RC_BSShaderTextureSet         >},
 | |
|         {"BSLODTriShape"                , &construct <BSLODTriShape               , RC_BSLODTriShape              >},
 | |
|         {"BSShaderProperty"             , &construct <BSShaderProperty            , RC_BSShaderProperty           >},
 | |
|         {"BSShaderPPLightingProperty"   , &construct <BSShaderPPLightingProperty  , RC_BSShaderPPLightingProperty >},
 | |
|         {"BSShaderNoLightingProperty"   , &construct <BSShaderNoLightingProperty  , RC_BSShaderNoLightingProperty >},
 | |
|         {"BSFurnitureMarker"            , &construct <BSFurnitureMarker           , RC_BSFurnitureMarker          >},
 | |
|         {"NiCollisionObject"            , &construct <NiCollisionObject           , RC_NiCollisionObject          >},
 | |
|         {"bhkCollisionObject"           , &construct <bhkCollisionObject          , RC_bhkCollisionObject         >},
 | |
|         {"BSDismemberSkinInstance"      , &construct <BSDismemberSkinInstance     , RC_BSDismemberSkinInstance    >},
 | |
|         {"NiControllerManager"          , &construct <NiControllerManager         , RC_NiControllerManager        >},
 | |
|         {"bhkMoppBvTreeShape"           , &construct <bhkMoppBvTreeShape          , RC_bhkMoppBvTreeShape         >},
 | |
|         {"bhkNiTriStripsShape"          , &construct <bhkNiTriStripsShape         , RC_bhkNiTriStripsShape        >},
 | |
|         {"bhkPackedNiTriStripsShape"    , &construct <bhkPackedNiTriStripsShape   , RC_bhkPackedNiTriStripsShape  >},
 | |
|         {"hkPackedNiTriStripsData"      , &construct <hkPackedNiTriStripsData     , RC_hkPackedNiTriStripsData    >},
 | |
|         {"bhkConvexVerticesShape"       , &construct <bhkConvexVerticesShape      , RC_bhkConvexVerticesShape     >},
 | |
|         {"bhkBoxShape"                  , &construct <bhkBoxShape                 , RC_bhkBoxShape                >},
 | |
|         {"bhkListShape"                 , &construct <bhkListShape                , RC_bhkListShape               >},
 | |
|         {"bhkRigidBody"                 , &construct <bhkRigidBody                , RC_bhkRigidBody               >},
 | |
|         {"bhkRigidBodyT"                , &construct <bhkRigidBody                , RC_bhkRigidBodyT              >},
 | |
|         {"BSLightingShaderProperty"     , &construct <BSLightingShaderProperty    , RC_BSLightingShaderProperty   >},
 | |
|         {"NiSortAdjustNode"             , &construct <NiSortAdjustNode            , RC_NiSortAdjustNode           >},
 | |
|         {"NiClusterAccumulator"         , &construct <NiClusterAccumulator        , RC_NiClusterAccumulator       >},
 | |
|         {"NiAlphaAccumulator"           , &construct <NiAlphaAccumulator          , RC_NiAlphaAccumulator         >},
 | |
|     };
 | |
| }
 | |
| 
 | |
| ///Make the factory map used for parsing the file
 | |
| static const std::map<std::string, CreateRecord> factories = makeFactory();
 | |
| 
 | |
| std::string NIFFile::printVersion(unsigned int 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 NIFFile::parse(Files::IStreamPtr&& stream)
 | |
| {
 | |
|     const std::array<std::uint64_t, 2> fileHash = Files::getHash(filename, *stream);
 | |
|     hash.append(reinterpret_cast<const char*>(fileHash.data()), fileHash.size() * sizeof(std::uint64_t));
 | |
| 
 | |
|     NIFStream nif (this, std::move(stream));
 | |
| 
 | |
|     // Check the header string
 | |
|     std::string head = nif.getVersionString();
 | |
|     static const std::array<std::string, 2> verStrings =
 | |
|     {
 | |
|         "NetImmerse File Format",
 | |
|         "Gamebryo File Format"
 | |
|     };
 | |
|     const bool supportedHeader = std::any_of(verStrings.begin(), verStrings.end(),
 | |
|         [&] (const std::string& verString) { return head.compare(0, verString.size(), verString) == 0; });
 | |
|     if (!supportedHeader)
 | |
|         fail("Invalid NIF header: " + head);
 | |
| 
 | |
|     // Get BCD version
 | |
|     ver = nif.getUInt();
 | |
|     // 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<uint32_t, 2> supportedVers =
 | |
|     {
 | |
|         NIFStream::generateVersion(4,0,0,0),
 | |
|         VER_MW
 | |
|     };
 | |
|     const bool supportedVersion = std::find(supportedVers.begin(), supportedVers.end(), ver) != supportedVers.end();
 | |
|     if (!supportedVersion)
 | |
|     {
 | |
|         if (sLoadUnsupportedFiles)
 | |
|             warn("Unsupported NIF version: " + printVersion(ver) + ". Proceed with caution!");
 | |
|         else
 | |
|             fail("Unsupported NIF version: " + printVersion(ver));
 | |
|     }
 | |
| 
 | |
|     // NIF data endianness
 | |
|     if (ver >= NIFStream::generateVersion(20,0,0,4))
 | |
|     {
 | |
|         unsigned char endianness = nif.getChar();
 | |
|         if (endianness == 0)
 | |
|             fail("Big endian NIF files are unsupported");
 | |
|     }
 | |
| 
 | |
|     // User version
 | |
|     if (ver > NIFStream::generateVersion(10,0,1,8))
 | |
|         userVer = nif.getUInt();
 | |
| 
 | |
|     // Number of records
 | |
|     const std::size_t recNum = nif.getUInt();
 | |
|     records.resize(recNum);
 | |
| 
 | |
|     // Bethesda stream header
 | |
|     // It contains Bethesda format version and (useless) export information
 | |
|     if (ver == VER_OB_OLD ||
 | |
|        (userVer >= 3 && ((ver == VER_OB || ver == VER_BGS)
 | |
|     || (ver >= NIFStream::generateVersion(10,1,0,0) && ver <= NIFStream::generateVersion(20,0,0,4) && userVer <= 11))))
 | |
|     {
 | |
|         bethVer = nif.getUInt();
 | |
|         nif.getExportString(); // Author
 | |
|         if (bethVer > BETHVER_FO4)
 | |
|             nif.getUInt(); // Unknown
 | |
|         nif.getExportString(); // Process script
 | |
|         nif.getExportString(); // Export script
 | |
|         if (bethVer == BETHVER_FO4)
 | |
|             nif.getExportString(); // Max file path
 | |
|     }
 | |
| 
 | |
|     std::vector<std::string> recTypes;
 | |
|     std::vector<unsigned short> recTypeIndices;
 | |
| 
 | |
|     const bool hasRecTypeListings = ver >= NIFStream::generateVersion(5,0,0,1);
 | |
|     if (hasRecTypeListings)
 | |
|     {
 | |
|         unsigned short recTypeNum = nif.getUShort();
 | |
|         if (recTypeNum) // Record type list
 | |
|             nif.getSizedStrings(recTypes, recTypeNum);
 | |
|         if (recNum) // Record type mapping for each record
 | |
|             nif.getUShorts(recTypeIndices, recNum);
 | |
|         if (ver >= NIFStream::generateVersion(5,0,0,6)) // Groups
 | |
|         {
 | |
|             if (ver >= NIFStream::generateVersion(20,1,0,1)) // String table
 | |
|             {
 | |
|                 if (ver >= NIFStream::generateVersion(20,2,0,5) && recNum) // Record sizes
 | |
|                 {
 | |
|                     std::vector<unsigned int> recSizes; // Currently unused
 | |
|                     nif.getUInts(recSizes, recNum);
 | |
|                 }
 | |
|                 const std::size_t stringNum = nif.getUInt();
 | |
|                 nif.getUInt(); // Max string length
 | |
|                 if (stringNum)
 | |
|                     nif.getSizedStrings(strings, stringNum);
 | |
|             }
 | |
|             std::vector<unsigned int> groups; // Currently unused
 | |
|             unsigned int groupNum = nif.getUInt();
 | |
|             if (groupNum)
 | |
|                 nif.getUInts(groups, groupNum);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     const bool hasRecordSeparators = ver >= NIFStream::generateVersion(10,0,0,0) && ver < NIFStream::generateVersion(10,2,0,0);
 | |
|     for (std::size_t i = 0; i < recNum; i++)
 | |
|     {
 | |
|         std::unique_ptr<Record> r;
 | |
| 
 | |
|         std::string rec = hasRecTypeListings ? recTypes[recTypeIndices[i]] : nif.getString();
 | |
|         if(rec.empty())
 | |
|         {
 | |
|             std::stringstream error;
 | |
|             error << "Record number " << i << " out of " << recNum << " is blank.";
 | |
|             fail(error.str());
 | |
|         }
 | |
| 
 | |
|         // Record separator. Some Havok records in Oblivion do not have it.
 | |
|         if (hasRecordSeparators && rec.compare(0, 3, "bhk"))
 | |
|         {
 | |
|             if (nif.getInt())
 | |
|             {
 | |
|                 std::stringstream warning;
 | |
|                 warning << "Record number " << i << " out of " << recNum << " is preceded by a non-zero separator.";
 | |
|                 warn(warning.str());
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         const auto entry = factories.find(rec);
 | |
| 
 | |
|         if (entry == factories.end())
 | |
|             fail("Unknown record type " + rec);
 | |
| 
 | |
|         r = entry->second();
 | |
| 
 | |
|         if (!supportedVersion)
 | |
|             Log(Debug::Verbose) << "NIF Debug: Reading record of type " << rec << ", index " << i << " (" << filename << ")";
 | |
| 
 | |
|         assert(r != nullptr);
 | |
|         assert(r->recType != RC_MISSING);
 | |
|         r->recName = rec;
 | |
|         r->recIndex = i;
 | |
|         r->read(&nif);
 | |
|         records[i] = std::move(r);
 | |
|     }
 | |
| 
 | |
|     const std::size_t rootNum = nif.getUInt();
 | |
|     roots.resize(rootNum);
 | |
| 
 | |
|     //Determine which records are roots
 | |
|     for (std::size_t i = 0; i < rootNum; i++)
 | |
|     {
 | |
|         int idx = nif.getInt();
 | |
|         if (idx >= 0 && static_cast<std::size_t>(idx) < records.size())
 | |
|         {
 | |
|             roots[i] = records[idx].get();
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             roots[i] = nullptr;
 | |
|             warn("Root " + std::to_string(i + 1) + " does not point to a record: index " + std::to_string(idx));
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // Once parsing is done, do post-processing.
 | |
|     for (const auto& record : records)
 | |
|         record->post(this);
 | |
| }
 | |
| 
 | |
| void NIFFile::setUseSkinning(bool skinning)
 | |
| {
 | |
|     mUseSkinning = skinning;
 | |
| }
 | |
| 
 | |
| bool NIFFile::getUseSkinning() const
 | |
| {
 | |
|     return mUseSkinning;
 | |
| }
 | |
| 
 | |
| std::atomic_bool NIFFile::sLoadUnsupportedFiles = false;
 | |
| 
 | |
| void NIFFile::setLoadUnsupportedFiles(bool load)
 | |
| {
 | |
|     sLoadUnsupportedFiles = load;
 | |
| }
 | |
| 
 | |
| }
 |