multitheftauto / mtasa-blue

Multi Theft Auto is a game engine that incorporates an extendable network play element into a proprietary commercial single-player game.
https://multitheftauto.com
GNU General Public License v3.0
1.38k stars 422 forks source link

Reimplement require (a way to import other resources .lua) #3551

Open ds1-e opened 2 months ago

ds1-e commented 2 months ago

Is your feature request related to a problem? Please describe.

I propose to recreate Lua's require and keep it as close as possible to original (default require is disabled since ever). It would be good idea to keep consistency with it, especially for those people who knew Lua before coming to MTA (it is common and widely used tool outside of it). For instance, this is how it is utilized in Project Zomboid:

1 2 3

Example taken directly from MTA: You have a dxDraw/utility function which gets used in few other resources. In order to not copy it over and over, and to keep codebase clean, you decide to create utility resource which will contain it. There are few ways to make use of it:

  1. You create export, then you call this exported function in render, in target resource - yes, however absurd and horrific it sounds, it happens. Your CPU won't like it, here's some details from botder, copied from Discord:

Please don't call exported functions for drawing like in onClient(Pre)Render MTA has to serialize the arguments passed, switch to the other Lua state, deserialize the arguments and call the exported function

  1. If you have been here for a while, you will probably find out about loadstring "solution":
loadstring(exports.dgs:dgsImportFunction())()

You need to add this long ass line to every resource, where utility function gets called.

  1. Eventually you'll get annoyed of doing that. If you are a bit more advanced in Lua, you could probably use your experience to create resource which will do that for you, and allow to import certain scripts.

Which leads us to the point, where there should be native support for it. Now, this is a moment when require steps in:

dxutils:

function dxDrawBorderedRectangle(...)
    -- do some draw calls
end

resourceA:

-- require "dxutils" is also valid
require("dxutils") -- import utils from given resource/lua file

-- now you can freely use dxDrawBorderedRectangle

function onClientRender()
    dxDrawBorderedRectangle(...)
end
addEventHandler("onClientRender", root, onClientRender)

You might want to keep things more organized, or simply not pollute _G, so:

mathutils:

local mathUtils = {}

function mathUtils.functionA(a, b)
    return (a * b)
end

function mathUtils.functionB(a, b)
    return (a/b)
end

return mathUtils

resourceB:

local mathUtils = require "mathutils"

print( mathUtils.functionA(10, 10) )

If you want to only access certain function, why not:

local mathFunctionB = require("mathutils").functionB

or:

local mathUtils = require("mathutils")
local mathFunctionB = mathUtils.functionB

It is also worth to mention that require isn't replacement to exports, but rather great addition. Since both of them have different purpose.

Describe the solution you'd like

requireLua / luaRequire

Describe alternatives you've considered

loadstring

Additional context

unknown

3128

Security Policy

Fernando-A-Rocha commented 2 months ago

Idea: Allow <script src=":resourceName/pathToScript.lua" type="client"/> to load scripts from other resources using the MTA file path style?

Fernando-A-Rocha commented 2 months ago

But also, why is require from Lua disabled? Clearly PZomboid managed to have it.

TracerDS commented 2 months ago

But also, why is require from Lua disabled? Clearly PZomboid managed to have it.

Security reasons

Proxy-99 commented 2 months ago

But also, why is require from Lua disabled? Clearly PZomboid managed to have it.

Security reasons

but what about server side scripts tho

Fernando-A-Rocha commented 2 months ago

https://www.tutorialspoint.com/lua/lua_modules.htm

ds1-e commented 2 months ago

Idea: Allow <script src=":resourceName/pathToScript.lua" type="client"/> to load scripts from other resources using the MTA file path style?

There's three more reasons why i've mentioned keep it as close as possible to original:

Fernando-A-Rocha commented 2 months ago

Thank you for the clarification. Yeah, screw the meta.xml importing. require is powerful, and has been tested on other platforms. We can make it secure and use it.

Fernando-A-Rocha commented 2 months ago

Speaking of modifying Lua, Luau exists... #2815

tederis commented 2 months ago

There's an another mechanism of scripts inclusion that we're using on a custom fork:

<meta>
    ...

    <include resource="library" >
        <script src="server.lua" type="server" />
        <script src="client.lua" type="client" />
    </include>
</meta>

It's just an extension of the resource inclusion mechanism. Easy to do and perfectly fit into the existing system.

TracerDS commented 2 months ago

There's an another mechanism of scripts inclusion that we're using on a custom fork:

<meta>
    ...

    <include resource="library" >
        <script src="server.lua" type="server" />
        <script src="client.lua" type="client" />
    </include>
</meta>

It's just an extension of the resource inclusion mechanism. Easy to do and perfectly fit into the existing system.

What if other resource would have the same library name (resouce="library")? Could there be more than 1 include field in 1 meta then?

Fernando-A-Rocha commented 2 months ago

There's an another mechanism of scripts inclusion that we're using on a custom fork:

<meta>
    ...

    <include resource="library" >
        <script src="server.lua" type="server" />
        <script src="client.lua" type="client" />
    </include>
</meta>

It's just an extension of the resource inclusion mechanism. Easy to do and perfectly fit into the existing system.

I like this. It's similar to what I suggested above having <script src=":resourceName/..." but this is better because the include tag already works for making sure the included resource starts before the meta.xml's resource, thus ensuring the included scripts belong to a running resource.

My question is: are the scripts included from another resource executed again in the LuaVM of the resource which is including them, or they are only ran once in the original resource?

Fernando-A-Rocha commented 2 months ago

What if other resource would have the same library name (resouce="library")? Could there be more than 1 include field in 1 meta then?

There can be as many includes in the meta.xml as you want. See wiki https://wiki.multitheftauto.com/wiki/Meta.xml

TracerDS commented 2 months ago

What if other resource would have the same library name (resouce="library")? Could there be more than 1 include field in 1 meta then?

There can be as many includes in the meta.xml as you want. See wiki https://wiki.multitheftauto.com/wiki/Meta.xml

I meant like, restricting include so there would be only 1 include per resource

Fernando-A-Rocha commented 2 months ago

Why would there need to be a maximum of 1 include? What if I wanna import multiple scripts from different libraries?

TracerDS commented 2 months ago

Why would there need to be a maximum of 1 include? What if I wanna import multiple scripts from different libraries?

Ah, my bad. I thought it will define a library for the require function

<meta>
    ...
    <include resource="library">
        <script src='...' />
    </include>
</meta>
require "library"

-- ...
Fernando-A-Rocha commented 2 months ago

I don't think tederis implemented "require" on his fork, but rather the meta.xml include scripts which I support.

tederis commented 2 months ago

There's an another mechanism of scripts inclusion that we're using on a custom fork:

<meta>
    ...

    <include resource="library" >
        <script src="server.lua" type="server" />
        <script src="client.lua" type="client" />
    </include>
</meta>

It's just an extension of the resource inclusion mechanism. Easy to do and perfectly fit into the existing system.

What if other resource would have the same library name (resouce="library")? Could there be more than 1 include field in 1 meta then?

As was already said, there could be as many includes as you want. The only limitation here is inability of cross inclusion when resources are referencing each others.

tederis commented 2 months ago

There's an another mechanism of scripts inclusion that we're using on a custom fork:

<meta>
    ...

    <include resource="library" >
        <script src="server.lua" type="server" />
        <script src="client.lua" type="client" />
    </include>
</meta>

It's just an extension of the resource inclusion mechanism. Easy to do and perfectly fit into the existing system.

I like this. It's similar to what I suggested above having <script src=":resourceName/..." but this is better because the include tag already works for making sure the included resource starts before the meta.xml's resource, thus ensuring the included scripts belong to a running resource.

My question is: are the scripts included from another resource executed again in the LuaVM of the resource which is including them, or they are only ran once in the original resource?

Included scripts are run in the same moment as native scripts do. That's guaranteed by an order of the resources startup.

Fernando-A-Rocha commented 2 months ago

@tederis I'm a fan of your method. Do you think this is a better alternative to reimplementing Lua require?

ds1-e commented 2 months ago

There's an another mechanism of scripts inclusion that we're using on a custom fork:

<meta>
    ...

    <include resource="library" >
        <script src="server.lua" type="server" />
        <script src="client.lua" type="client" />
    </include>
</meta>

It's just an extension of the resource inclusion mechanism. Easy to do and perfectly fit into the existing system.

As i've mentioned on Discord, require imho has superior way over it, because it allows to import code to the place where it was called, not only to the whole resource. Regarding more security aspect, there could be a flag in meta.xml which will mark resource possible to require from other resources.

ds1-e commented 2 months ago

I've got some more insights on how it works, much thanks goes to @Niventill who elaborated on it. Main post got updated with MTA example, and require example usages, so please re-read it.

One of @TracerDS concerns was that _G would get polluted, in require you can easily solve it (see mathutils example). When it comes to meta.xml approach, it gets tricky, because code would be loaded to each script of target resource. Well, perhaps you could add importTo attribute:

<meta>
    ...

    <include resource="library" >
        <script src="server.lua" importTo="serverA.lua" type="server" />
        <script src="client.lua" importTo="clientB.lua" type="client" />
    </include>
</meta>

Yes, but it makes things a bit complicated, and you probably can see in which direction it is going. Not a convenient one, for sure. I believe there was a reason why require stayed nearly the same (excluding obvious security changes), on all the aforementioned platforms.

@tederis I think it is fair to say that point goes in favor of require.

tederis commented 2 months ago

@tederis I'm a fan of your method. Do you think this is a better alternative to reimplementing Lua require?

For my 15 years of experience in MTA, I'm inclined to say that require is excessive. I usually expect a global(for the entire VM) import in roughly 9 cases of 10 (again, from my experience). To prevent _G from pollution a library script can be wrapped into a table, that's it. I'm not against require, it's just impractical for me. But it can be an alternative to what I said, perhaps. I rather worried about implementation difficulties. My approach relies on the existing system which guarantees that a library script will be loaded before the dependent one. On the other side, require will require(sorry for the pun) significant work to make client scripts work correctly.

dmi7ry commented 2 months ago

On the other side, require will require(sorry for the pun) significant work to make client scripts work correctly.

Moreover, something similar to a regular require can be done using file functions and loadstring.