diff --git a/dUgcServer/UgcFormats.cpp b/dUgcServer/UgcFormats.cpp index 28f114a40..64b43f275 100644 --- a/dUgcServer/UgcFormats.cpp +++ b/dUgcServer/UgcFormats.cpp @@ -118,6 +118,12 @@ namespace { // NiAVObject flags as the game's own brick models (res/BrickModels/ndmade) have them: nodes 0x110, shapes 0x10 constexpr uint16_t NODE_FLAGS = 0x110; constexpr uint16_t SHAPE_FLAGS = 0x10; + // Nodes and shapes with controllers under them, as the client's own animated files (the AG ocean): the client + // updates an object's scene graph every frame only when its root has the selective update bit (0x02; + // LWOBaseRenderComponent::Run 0x00d5d770, NiAVObject::GetSelectiveUpdate 0x00413050). Without it the model is + // updated once when it loads and its controllers never move. + constexpr uint16_t ANIMATED_NODE_FLAGS = 0x102; + constexpr uint16_t ANIMATED_SHAPE_FLAGS = 0x1A; // selective update, update property controllers, rigid void WriteNet(Writer& out, int32_t name) { out.I32(name); @@ -201,9 +207,9 @@ namespace { return std::move(out.Data()); } // An NiNode's data: no properties, `children`, no effects - std::string NodeData(int32_t name, const std::vector& children) { + std::string NodeData(int32_t name, const std::vector& children, uint16_t flags = NODE_FLAGS) { Writer node; - WriteAv(node, name, {}); + WriteAv(node, name, {}, flags); node.U32(static_cast(children.size())); for (const auto child : children) node.I32(child); node.U32(0); // effects @@ -389,7 +395,8 @@ namespace { const auto shapeBlock = m_Nif.Reserve("NiTriShape"); const auto dataBlock = m_Nif.Add("NiTriShapeData", TriShapeData(*mesh, glitter)); Writer tri; - WriteAv(tri, m_Nif.String(name), properties, SHAPE_FLAGS); + const bool animated = glitter && (glitter->PeriodU() > 0.0f || glitter->PeriodV() > 0.0f); + WriteAv(tri, m_Nif.String(name), properties, animated ? ANIMATED_SHAPE_FLAGS : SHAPE_FLAGS); tri.I32(dataBlock); tri.I32(-1); // skin instance tri.U32(0); // materials @@ -429,8 +436,12 @@ namespace UgcFormats { const int32_t root = nif.Reserve("NiNode"); SharedProperties properties(nif); std::vector groupBlocks; + bool anyAnimated = false; for (const auto& group : groups) { if (group.lods.empty()) continue; + const bool animated = group.glitter && (group.glitter->PeriodU() > 0.0f || group.glitter->PeriodV() > 0.0f); + anyAnimated = anyAnimated || animated; + const auto nodeFlags = animated ? ANIMATED_NODE_FLAGS : NODE_FLAGS; const auto lodNode = nif.Reserve("NiLODNode"); std::vector levels; Writer ranges; @@ -443,13 +454,13 @@ namespace UgcFormats { const auto block = properties.Shape(group.name, piece, group.transparent, group.emissive, group.glitter); if (block >= 0) shapes.push_back(block); } - nif.Fill(level, NodeData(nif.String(lod.name), shapes)); + nif.Fill(level, NodeData(nif.String(lod.name), shapes, nodeFlags)); levels.push_back(level); ranges.Float(lod.nearDistance); ranges.Float(lod.farDistance); } const auto rangeData = nif.Add("NiRangeLODData", std::move(ranges.Data())); - auto data = NodeData(nif.String(group.name), levels); + auto data = NodeData(nif.String(group.name), levels, nodeFlags); Writer lod; lod.Raw(data); lod.U16(3); // switch flags: update only the active child, and controllers (as the game's own files) @@ -458,7 +469,7 @@ namespace UgcFormats { nif.Fill(lodNode, std::move(lod.Data())); groupBlocks.push_back(lodNode); } - nif.Fill(root, NodeData(nif.String(rootName), groupBlocks)); + nif.Fill(root, NodeData(nif.String(rootName), groupBlocks, anyAnimated ? ANIMATED_NODE_FLAGS : NODE_FLAGS)); return nif.Finish(root); } diff --git a/docs/UgcServer.md b/docs/UgcServer.md index 5b9d8fd20..bda74669c 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -299,7 +299,12 @@ What a glitter shape has, beside what plastic shapes have (white material, alpha base map, operation translate U and translate V, each with an `NiFloatInterpolator` and `NiFloatData` of two linear keys (0, 0) and (period, 1): a tile in `7 / glitter_speed` s in U and `11 / glitter_speed` s in V, looping, and wrapping makes the loop seamless. The block layouts are the ocean file's (its controllers are 39 bytes, the property - 70). With `glitter_speed` 0 the property has no controllers. + 70). With `glitter_speed` 0 the property has no controllers. The client updates an object's scene graph every frame only when its root + NiNode has the selective update bit (0x02; `LWOBaseRenderComponent::Run` 0x00d5d770 calls `NiAVObject::Update` when + `NiAVObject::GetSelectiveUpdate` 0x00413050 is set); otherwise only once when it loads, and the controllers never + move. So a model with moving glitter has flags 0x102 on its root, the glitter NiLODNode and its `LOD_n` nodes, and + 0x1A on the glitter shapes, as the client's own AG ocean (`mesh/env/env_ag_ocean-maelstrom.nif`); every other + node keeps 0x110 and shape 0x10. The client finds the animation: `SetupRenderNodeExtraData` (0x00c746c0) sets `RenderNodeExtraData.flags0` bit 2 from `NifHasAnimatedControllers` (0x00bf4160), which returns true for a shape whose `NiTexturingProperty`'s first diff --git a/tests/dUgcTests/UgcTests.cpp b/tests/dUgcTests/UgcTests.cpp index 45aaa498e..5535a5ad9 100644 --- a/tests/dUgcTests/UgcTests.cpp +++ b/tests/dUgcTests/UgcTests.cpp @@ -1347,6 +1347,71 @@ namespace { // A glitter group: UVs, the fleck texture stored in the file, and the two texture transform controllers the client // animates it with; read back as NifFile sees it +namespace { + // Each block's type and its NiAVObject flags (the u16 after the name, extra data count and controller), from a + // NIF 20.3.0.9 as UgcFormats writes it (no extra data) + std::vector> BlockFlags(const std::string& nif) { + size_t at = nif.find('\n') + 1; + const auto u32 = [&]() { uint32_t v = 0; std::memcpy(&v, nif.data() + at, 4); at += 4; return v; }; + const auto u16 = [&]() { uint16_t v = 0; std::memcpy(&v, nif.data() + at, 2); at += 2; return v; }; + at += 4 + 1 + 4; // version, endian, user version + const auto blocks = u32(); + const auto typeCount = u16(); + std::vector types; + for (uint16_t i = 0; i < typeCount; i++) { + const auto length = u32(); + types.emplace_back(nif.substr(at, length)); + at += length; + } + std::vector blockTypes; + for (uint32_t i = 0; i < blocks; i++) blockTypes.push_back(u16() & 0x7fff); + std::vector sizes; + for (uint32_t i = 0; i < blocks; i++) sizes.push_back(u32()); + const auto strings = u32(); + u32(); // max length + for (uint32_t i = 0; i < strings; i++) at += u32(); + const auto groups = u32(); + at += groups * 4; + std::vector> out; + for (uint32_t i = 0; i < blocks; i++) { + uint16_t flags = 0; + const auto& type = types[blockTypes[i]]; + if (type == "NiNode" || type == "NiLODNode" || type == "NiTriShape") std::memcpy(&flags, nif.data() + at + 12, 2); + out.emplace_back(type, flags); + at += sizes[i]; + } + return out; + } +} + +// The client updates an object's scene graph every frame only when its root has the selective update bit (0x02), so +// a model with moving glitter has it on its root, the glitter group's nodes and shapes (as the client's own animated +// files); still glitter and everything else keep the game's brick model flags +TEST(UgcFormats, MovingGlitterIsUpdatedEveryFrame) { + const auto mesh = Quad({ 0.2f, 0.4f, 0.8f, 0.6f }); + const UgcGlitter::Params moving{ 1.6f, 50, 1.0f }; + const UgcGlitter::Params still{ 1.6f, 50, 0.0f }; + for (const auto* glitter : { &moving, &still }) { + const auto nif = UgcFormats::WriteLodNif("SceneNode_Model", { + { "S01_Opaque_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } } }, + { "S21_Glitter_Model", false, { { 0.0f, 100.0f, "LOD_0", { &mesh } } }, 0.0f, glitter } }); + const auto blocks = BlockFlags(nif); + const bool animated = glitter == &moving; + ASSERT_EQ(blocks[0].first, "NiNode"); + EXPECT_EQ(blocks[0].second, animated ? 0x102 : 0x110) << "root"; + std::vector shapes; + for (const auto& [type, flags] : blocks) if (type == "NiTriShape") shapes.push_back(flags); + ASSERT_EQ(shapes.size(), 2u); + EXPECT_EQ(shapes[0], 0x10); // plastic + EXPECT_EQ(shapes[1], animated ? 0x1A : 0x10); + std::vector lods; + for (const auto& [type, flags] : blocks) if (type == "NiLODNode") lods.push_back(flags); + ASSERT_EQ(lods.size(), 2u); + EXPECT_EQ(lods[0], 0x110); + EXPECT_EQ(lods[1], animated ? 0x102 : 0x110); + } +} + TEST(UgcFormats, GlitterNifReadsBack) { const auto mesh = Quad({ 0.2f, 0.4f, 0.8f, 0.6f }); const UgcGlitter::Params glitter{ 1.6f, 50, 2.0f };