MafiaHub / Framework

Advanced modding framework for multiplayer modifications
https://mafiahub.dev
Other
37 stars 7 forks source link

Scripting layer and the case of supporting multiple scripting engines #83

Closed zpl-zak closed 5 days ago

zpl-zak commented 6 months ago

Problem

We currently only support the Node.js engine, but if we also plan to embed either the Squirrel or Lua VM, we need to re-think how we bind built-ins to our VMs.

In the current implementation, we call the mod-level ModuleRegister method and pass down a wrapper that would contain the initialized scripting engine. The following snippet is an example of us rejecting all VMs except Node.js and then calling the respective Builtins::Register method, which fully anticipates the Node.js engine:

void Server::ModuleRegister(Framework::Scripting::Engines::SDKRegisterWrapper sdk) {
    if (sdk.GetKind() != Framework::Scripting::ENGINE_NODE)
        return;

    const auto nodeSDK = sdk.GetNodeSDK();
    MafiaMP::Scripting::Builtins::Register(nodeSDK->GetIsolate(), nodeSDK->GetModule());
}

A better portion of the built-ins is, however, engine-agnostic, as can be observed here: devenv_AleRqNvyy9

That is thanks to the v8pp library that only requires a small footprint to register our class with the VM: devenv_5czyLVHuPU

Despite that, we still have several methods that fully assume we rely only on Node.js and are thus incompatible with the other engines: devenv_BlUV7hfDpZ

Events are the biggest offender:

static void EventPlayerDied(flecs::entity e) {
    auto engine = reinterpret_cast<Framework::Scripting::Engines::Node::Engine*>(Framework::CoreModules::GetScriptingModule()->GetEngine());
    auto playerObj = WrapHuman(engine, e);
    engine->InvokeEvent("playerDied", playerObj);
}

static void EventPlayerConnected(flecs::entity e) {
    auto engine = reinterpret_cast<Framework::Scripting::Engines::Node::Engine*>(Framework::CoreModules::GetScriptingModule()->GetEngine());
    auto playerObj = WrapHuman(engine, e);
    engine->InvokeEvent("playerConnected", playerObj);
}

static void EventPlayerDisconnected(flecs::entity e) {
    auto engine = reinterpret_cast<Framework::Scripting::Engines::Node::Engine*>(Framework::CoreModules::GetScriptingModule()->GetEngine());
    auto playerObj = WrapHuman(engine, e);
    engine->InvokeEvent("playerDisconnected", playerObj);
}

Discussion

We need to find a way to allow other engines to reuse the existing binding layer without introducing too much friction and code duplication. Ideally, bindings should be written only once. Then, the engine-specific wrappers should do the rest of the job, avoiding the mistake of having multiple copies of the same API to maintain per engine.

Scope

Scripting layer and MafiaMP's bindings.

zpl-zak commented 5 days ago

This issue can be closed as we've opted for a simplified single-language model (Node.js).