Closed Deewarz closed 3 months ago
Hello @Deewarz ,
This is a nicely written issue. Can you please elabore on the object as argument
part? I'm not sure if i understand the reasoning for this.
Hello @Deewarz ,
This is a nicely written issue. Can you please elabore on the
object as argument
part? I'm not sure if i understand the reasoning for this.
Hello @martonp96, thank you for your comment!
I mean you only get a single argument for your listener (which we could call the context (ctx
) for example)
The context argument is always an object that can receive different keys, which allows destructuring.
So, it's as if I had written:
Resources.on('loaded', (ctx) => {
const { resource } = ctx
console.log(`${resource.name} is loaded`)
})
A first example to understand the interest with the Commands module. In the context, you get the player instance and the command instance.
If you don't need the player, you don't have to write it in your code:
Commands.on('test', ({ command }) => {
console.log(`${command.name} is executed.`)
})
2nd example with a custom event where I would only need some of the arguments:
// client side
player.emitNet('myEvent', { foo: true, bar: false, plop: 1})
// server side
Players.on('myEvent', ({ foo }) => { // ignore player, bar, plop
console.log(foo)
})
Players.on('myEvent', ({ plop }) => { // ignore player, foo, bar
console.log(plop)
})
// Otherwise i should define arguments that I don't need
Players.on('myEvent', (player, foo, bar, plop) => { // i don't need player, foo, bar
console.log(plop)
})
Finally, there is also an interest for the Scripting API maintainers because they can add things to the context of a module listener without causing a breaking change in the signature of the listener.
Have I been clear enough? Otherwise do not hesitate to ask :)
Finally, there is also an interest for the Scripting API maintainers because they can add things to the context of a module listener without causing a breaking change in the signature of the listener.
This can also be done by adding new arguments to the end of the argument list. I think this makes the API look really different from the usual JS library experience where you get multiple arguments, though this is of course something where everyone has a different taste. I just think that this principle of only passing one argument which is an object, is not really nice style.
All methods is, or starts with, a verb. (getXX, setXX, spawn, ...) (don't player.position but player.getPosition)
Whats the reason for doing this? This is of course again just taste, but I think getters and setters are a nice feature of JS that should be utilized here. The getX
and setX
approach is just what you would have used in older languages (which don't have setters / getters like C++ etc.) so maybe this is just something out of habit.
@Deewarz is it inspired by some existing solution?
This can also be done by adding new arguments to the end of the argument list. I think this makes the API look really different from the usual JS library experience where you get multiple arguments, though this is of course something where everyone has a different taste. I just think that this principle of only passing one argument which is an object, is not really nice style.
@LeonMrBonnie I think it was definitely a case before. But seems nowadays it is indeed more common to see the object unpacking mechanism, specifically for event arguments, just like @Deewarz pointed out. Also it makes sense from the other side when that same object is being the argument for the event transmitter.
@LeonMrBonnie, @inlife Thanks for your comments!
To @LeonMrBonnie + @inlife:
This can also be done by adding new arguments to the end of the argument list. I think this makes the API look really different from the usual JS library experience where you get multiple arguments, though this is of course something where everyone has a different taste. I just think that this principle of only passing one argument which is an object, is not really nice style.
@LeonMrBonnie I think it was definitely a case before. But seems nowadays it is indeed more common to see the object unpacking mechanism, specifically for event arguments, just like @Deewarz pointed out. Also it makes sense from the other side when that same object is being the argument for the event transmitter.
To @LeonMrBonnie:
All methods are, or start with, a verb. (getXX, setXX, spawn, ...) (don't player.position but player.getPosition)
Whats the reason for doing this? This is of course again just taste, but I think getters and setters are a nice feature of JS that should be utilized here. The
getX
andsetX
approach is just what you would have used in older languages (which don't have setters / getters like C++ etc.) so maybe this is just something out of habit.
Originally, I had considered using the Getter & Setter, but I changed my mind because it is more explicit to do it this way for two main reasons:
For example, I want to manipulate the player position:
player.setPosition()
and player.getPosition()
methodsplayer.getPosition()
methodIf I use the native get / set I only have player.position
(I don't explicitly know if I can use the setter or not)
Finally, the choice to always use a verb is mostly to keep consistency in API (eg. player.sendChat()
instead of player.chat()
)
To @inlife:
@Deewarz is it inspired by some existing solution?
Not really, not an existing API but simply inspired by my professional experience and my thoughts on other scripting APIs from other games.
Hi, thank you for the RFC contrib!
Never instantiate but always use a factory (do World.createVehicle(...) instead of new Vehicle(...))
At first, I wasn't sure if this rule is necessary, but when I look at it from the user's POV, this rule ensures that we make it explicit the object created is not owned by the script itself but rather by the MP layer the request is queried to that provides us a handle to it. This can clear out confusion where one might consider storing the object instance in the event of a possible GC, which in reality does not affect the data itself. By establishing a rule only to allow object creation via factories, we help users know the MP side owns the data and the script only retrieves a handle to it.
All "scripting modules" can listen for events (and some of them can emit) (also to the net)
This slightly contradicts the idea behind OOP first API as the events should actually be overriding base event listeners to extend functionality:
MyModPlayer.prototype.died = ctx -> {}
However, given the nature of the scripting API, the NAPI part is not aware of a script-level inherited class that would provide such extension (e.g. NAPI doesn't know MyModPlayer class is a thing unless we explicitly state that on script-level), this is a very fair compromise and the use of message-based event processing via a static method very much makes sense. This solution is the equivalent of passing the object instance into a static class member method and should work well in that case.
tl;dr, this is a good approach.
You can attach data to entities (and you can synchronize them with the client)
This is a good idea. NAPI objects (such as: Player, Vehicle instances) only serve as reference objects holding the bare minimum of data to identify their native persistent counterpart. The ability to store custom data on the mp-level side (via setData
) also allows us to transmit and share data across multiple scripting instances or even over the network.
All methods are, or start with, a verb. (getXX, setXX, spawn, ...) (don't player.position but player.getPosition)
This is a valid proposition. Even if properties would make more sense from a syntax standpoint, semantically, it makes more sense if data is accessed via regular getters/setters.
Apart from what you've already mentioned, the reasoning behind this also goes into an understanding of how to work with the data NAPI provides us. The MP doesn't serve us a full-blown representation of a native object. It merely provides us with an object that refers/identifies it.
Properties assume the data you work with is persistent, but that isn't the case since you don't work with the raw native object. Having explicit getters/setters help greatly as it establishes a strong division between what we should consider as immutable temporal data served only to identify native data or what we present as native data on its own.
Consistency also plays a role in establishing a strong API which ensures the API user instinctively knows how to access and manipulate the data provided.
We use properties for immutable values (eg. player.id)
This supports the previous statement, as it marks a clear division between native data representation and the mere reference to it. I would, however, consider reducing this scope purely into identification data necessary to communicate with the native counterpart.
Is there anything else we could discuss?
NOTE: I'm moving this issue over to the Framework as MafiaMP depends on it directly.
Summary
The objective of this RFC (Request for Comments) is to jointly define the vision for Scripting API.
I have already thought about it and I would like to share you an example.
It's possible that this implementation is complicated to implement or requires a JS wrapper on top of the NAPI layer. That's why I'm starting this thread to see together what can be done.
For now, I focus on the server side.
Goals
World.createVehicle(...)
instead ofnew Vehicle(...)
)player.id
)Example