Jump to content

Working SMP Actor->Actor leash system prototype


Recommended Posts

Posted

Been messing around with lot of Skyrim's code lately and found out you can pretty easily manipulate SMP objects. 

 

Here is a leash system where I've created a simple IK end-effector controller:

 

 
As you can see, there's some flickering going on. No idea why, but setting restitution to 0 per MDLash node seems to reduce it significantly. It's almost like there's a weird race issue going on

Here's the code - super simple:
 
Spoiler
const std::string TARGET_SUFFIX = "MDLash 12"; //The node has random ass names words/characters included in it, so need to scan by suffix
const float MAX_DISTANCE = 200.0f;

bool EndsWith(const std::string& fullString, const std::string& ending) {
    if (fullString.length() >= ending.length()) {
        return (0 == fullString.compare(fullString.length() - ending.length(), ending.length(), ending));
    }
    return false;
}

RE::NiNode* FindNodeBySuffix(RE::NiNode* a_root, const std::string& a_suffix) {
    if (!a_root) return nullptr;
    if (EndsWith(a_root->name.c_str(), a_suffix)) return a_root;
    for (auto& child : a_root->GetChildren()) {
        if (child) {
            auto found = FindNodeBySuffix(child->AsNode(), a_suffix);
            if (found) return found;
        }
    }
    return nullptr;
}

RE::NiNode* FindNodeExact(RE::NiNode* a_root, const char* a_name) {
    if (!a_root) return nullptr;
    if (_stricmp(a_root->name.c_str(), a_name) == 0) return a_root;
    for (auto& child : a_root->GetChildren()) {
        if (child) {
            auto found = FindNodeExact(child->AsNode(), a_name);
            if (found) return found;
        }
    }
    return nullptr;
}

RE::Actor* FindNearestNPC(RE::Actor* a_player) {
    RE::Actor* nearest = nullptr;
    float minMsgSq = MAX_DISTANCE * MAX_DISTANCE; 

    auto processLists = RE::ProcessLists::GetSingleton();
    if (!processLists) return nullptr;

    for (auto& handle : processLists->highActorHandles) {
        auto actorPtr = handle.get();
        if (actorPtr && actorPtr.get()) {
            RE::Actor* actor = actorPtr.get();

            if (!actor || actor == a_player || !actor->Is3DLoaded()) continue;

            float distSq = a_player->GetPosition().GetSquaredDistance(actor->GetPosition());

            if (distSq < minMsgSq) {
                minMsgSq = distSq;
                nearest = actor;
            }
        }
    }

    return nearest;
}

void OnPlayerUpdate() {

    auto player = RE::PlayerCharacter::GetSingleton();
    if (!player || !player->Is3DLoaded()) return;

    auto root3D = player->Get3D(false)->AsNode();
    if (!root3D) return;

    // Find the Lash Node on the Player
    auto lashNode = FindNodeBySuffix(root3D, TARGET_SUFFIX);
    if (!lashNode) return;

    RE::Actor* targetActor = FindNearestNPC(player);

    // If no NPC is within 200 units then skip since the shit isn't long
    if (!targetActor) return;

    auto npcRoot = targetActor->Get3D(false)->AsNode();
    if (!npcRoot) return;

    RE::NiNode* handNode = FindNodeExact(npcRoot, "NPC R Hand [RHnd]");
    if (!handNode) {
        handNode = FindNodeExact(npcRoot, "NPC L Hand [LHnd]"); 
    }

    RE::NiPoint3 targetWorldPos;

    if (handNode) {
        targetWorldPos = handNode->world.translate;
    } else {
        // No hands for whatever reason
        targetWorldPos = targetActor->GetPosition();
        targetWorldPos.z += 100.0f;  // Maybe like the middle of the actor idfk
    }

    //convert World-Space-Target to Local-Space (Relative to Lash Node's Parent)
    auto parent = lashNode->parent;
    if (parent) {
        RE::NiTransform parentWorld = parent->world;

        // difference
        RE::NiPoint3 diff = targetWorldPos - parentWorld.translate;

        // Apply inverse rotation of parent
        RE::NiPoint3 localPos = parentWorld.rotate.Transpose() * diff;

        // Apply inverse scale of parent
        if (parentWorld.scale != 0.0f) {
            localPos = localPos / parentWorld.scale;
        }

        lashNode->local.translate = localPos;

        // Must force an update or it wont display our updated position...
        RE::NiUpdateData ctx;
        ctx.time = 0.0f;
        ctx.flags = static_cast<RE::NiUpdateData::Flag>(1);  //kDirty

        lashNode->UpdateWorldData(&ctx);
    }
}


// Hook logic, i register this on kDataLoaded
//Should be a great place to manipulate node positions?
class PlayerUpdateHook {
public:
    static void Install() {
        // Hook PlayerCharacter::Update
        REL::Relocation<std::uintptr_t> PlayerVTable{RE::VTABLE_PlayerCharacter[0]};
        _Update = PlayerVTable.write_vfunc(0xAD, Update);
    }

private:
    static void Update(RE::PlayerCharacter* a_this, float a_delta) {
        _Update(a_this, a_delta);
        OnPlayerUpdate();  
    }

    static inline REL::Relocation<decltype(Update)> _Update;
};

 



Now, my current basic code completely ignores Skyrim's SMP system. Which makes the leash system a lot less accurate looking with gravity,etc since the physics system uses it's own transforms for the bone which are de-synced from my manual code.

Setting the end node to a kinematic object fixes it, and the IK solver works properly - but then SMP freaks the fuck out with physics and creates spasms if the leash is stretched further then it is. 

 

So you could eventually tune this lease thing to work really well and look good in-game. It'd just be a bit tedious. IMO it would be easier to keep the end node as a physics object still, but hook into HdtSMP and modify the end node's smp to follow the hand. There's even some API for easy hooking: https://github.com/DaymareOn/hdtSMP64/blob/master/hdtSMP64/PluginAPI.h

 

https://github.com/DaymareOn/hdtSMP64/blob/master/hdtSMP64/BulletCollision/CollisionDispatch/btCollisionObject.h

 

Aaannyways. Neat stuff. Might make a little API for papyrus plugins to leash actors some day. But the only leash I've ever seen on Skyrim is the one from Cosplay pack so 

Posted
42 minutes ago, asdt123123 said:

Aaannyways. Neat stuff. Might make a little API for papyrus plugins to leash actors some day. But the only leash I've ever seen on Skyrim is the one from Cosplay pack so 

Duskbound and Invicta Couture Lingerie immediately come to mind for other outfits with leashes.

 

This looks really interesting.

Posted
19 minutes ago, shrtjsrtj said:

Duskbound and Invicta Couture Lingerie immediately come to mind for other outfits with leashes.

 

This looks really interesting.

Ahh nice find, those are pretty long too. 

 

If the leash system is fully integrated into the SMP system rather then relying on IK like what I'm doing, those chains would work out great. 

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...