Lua scripting in OpenMW

Everything about development and the OpenMW source code.
Post Reply
ptmikheev
Posts: 69
Joined: 01 Jun 2020, 21:05
Gitlab profile: https://gitlab.com/ptmikheev

Lua scripting in OpenMW

Post by ptmikheev »

Existing scripting systems in Morrowind

Scripting in vanilla Morrowind (mwscript)

Every active script is executed every frame (i.e. circa 20-60 times a second). If something should be evaluated only once, a control variable should be used. A small example of a script:

Code: Select all

Begin small_example_script
Short alreadyActivated
If (OnActivate == 1)
    If (alreadyActivated == 0)
        MessageBox "Hello, World!"
        Set alreadyActivated to 1
    endif
endif
End
There are local scripts and global scripts.
Any script that is running on an object or Actor in the game is a local script. Local scripts are only active if the cell is loaded – this is the current interior cell, or the current and all directly neighboring exterior cells. When the object is outside of this range, the script is not running, but the local variables are saved.
Any script that is not attached to any object is a global script. Global scripts are active all the time once they have been activated and until they are specifically terminated.

Note: information from http://wiki.theassimilationlab.com/mmw/ ... or_Dummies was used here. See the link for more details.

OpenMW fully supports vanilla mwscript. But the vanilla scripting is inconvenient and has a lot of limitations. For example it is impossible to attach several scripts to a single actor.

MWSE Lua scripting

Morrowind Script Extender adds new functions to the mwscript language and also supports event-based Lua scripts. Here is an example of a simple script (from https://mwse.readthedocs.io/en/latest/l ... a-mod.html):

Code: Select all

 -- The function to call on the showMessageboxOnWeaponReadied event.
 local function showMessageboxOnWeaponReadied(e)
     -- Exit the function is the actor is not the player.
     if (e.reference ~= tes3.player) then
         return
     end

     -- Locally store the weapon reference being readied in the event.
     local weaponStack = e.weaponStack

     -- Check that the reference exists and the reference object is a two-handed weapon.
     if (weaponStack and weaponStack.object.isTwoHanded) then
         -- Print our statement.
         tes3.messageBox("I just drew " .. weaponStack.object.name .. ", destroyer of worlds!")
     end
 end

 -- The function to call on the initialized event.
 local function initialized()
     -- Register our function to the onReadied event.
     event.register("weaponReadied", showMessageboxOnWeaponReadied)

     -- Print a "Ready!" statement to the MWSE.log file.
     print("[MWSE Guide Demo: INFO] MWSE Guide Demo Initialized")
 end

 -- Register our initialized function to the initialized event.
 event.register("initialized", initialized)
MWSE is not supported by OpenMW (and can not be supported due to low level differences in the engine). It is the main reason why some mods don’t work in OpenMW.

Lua scripting in TES3MP

Currently in TES3MP server has no game assets. All game mechanics are handled on a client side. Every client calculates the behavior of some actors (let’s call them locally controlled actors) and sends coordinates to others. Server only transfers coordinates and state of all non-static objects between players and applies scripts to this data. So scripts are server-side only. It has some fundamental limitations. I.e. scripts can add/remove objects, but can not do anything that requires game assets (like ray tracing, path finding, AI behavior and so on).

On the client side (as well as in single player OpenMW) the state of the world is mixed with game logic. Main data structures are defined in apps/OpenMW/mwworld and apps/OpenMW/mwmechanics. Let’s call it “OpenMW data model”.

On the server side of TES3MP the state of the world is stored using classes from components/openmw-mp/Base. Let’s call it “TES3MP-server data model”. Scripts can directly access this data via global arrays. I.e. “TES3MP script data model” is identically equal to “TES3MP-server data model”.

Lua scripting is an essential part of TES3MP. Significant part of basic server functionality is implemented via scripts and TES3MP can not work without it. Here is an example of a simple script “limiting players' level” (from https://github.com/tes3mp/CoreScripts/b ... utorial.md):

Code: Select all

local maxLevel = 20
customEventHooks.registerValidator("OnPlayerLevel", function(eventStatus, pid)
    local player = Players[pid]
    if player.data.stats.level >= maxLevel then
        player.data.stats.level = maxLevel
        player.data.stats.levelProgress = 0
        player:LoadLevel()
        --cancel the level increase on the server side
        --there have been no level up anymore, so don't run custom handlers for it either
        return customEventHooks.makeEventStatus(false,false) 
    end
end)
Ways to add Lua scripting to OpenMW

A) What if we just add TES3MP scripting to OpenMW?

Adding TES3MP scripting to OpenMW in exactly the same form as it is now never was a plan, because it would lead to the following problems:
  • Since scripting requires data in the server-side format we need to do a conversion from “OpenMW data model” to “TES3MP-server data model” even in single player. In other words in single player we need to gather the data the same way as if it were multiplayer, simulate sending it to a server and only then apply scripts. It is quite a big amount of work for developers and it requires merging half of TES3MP.
  • Converting world state to the TES3MP-server data model also has a significant performance cost.
  • Adding new functions in many cases requires an enormous amount of work. For example if we want to tweak camera behavior via scripting, we need to create a special server-side class for the camera (that's actually useless for multiplayer), modify the client-server protocol and so on. Many changes in many places for a trivial functionality. It is even less convenient than adding new functions to the vanilla mwscript.
  • Due to its limitations the scripting system will not fully meet expectations about it. If we implement it this way, quite a lot of things will not be doable through scripting (or will require complicated and ugly workarounds). It includes custom ai packages, camera tweaks, and anything that depends on game assets. It is worth mentioning that there is a pull request that adds custom ui support to TES3MP. It is quite big and can be an example of a “complicated workaround”.
So TES3MP scripting should be considered as a prototype rather than a final solution for OpenMW. It has to be significantly changed first.

B) What if we forget about TES3MP and implement a new Lua scripting from scratch?

It is possible to implement Lua scripting from scratch without thinking about multiplayer at all. To be honest it can be very simple and very powerful. The huge problem here is that it would significantly increase the gap between OpenMW and TES3MP. I.e. TES3MP would have to deal with two incompatible scripting systems at the same time. In this case OpenMW will never support multiplayer and there is a high probability that TES3MP will die at some point despite its users currently making up a significant share of OpenMW's audience.

C) Combined scripting system

The actual plan is to develop a new universal scripting system that will be convenient for both OpenMW and TES3MP.

Let’s extend the concept of global and local scripts from mwscript (see above). In the new approach both of them will be written in Lua, but:
  • Global script is something similar to what TES3MP scripts are now. It can register handlers for a wide variety of events, access coords of all non-static objects (doesn't matter is it within loaded cells or not), but can not do things that require game assets (pathfinding, raytracing, direct AI control and so on). Global scripts can attach/detach local scripts to any object.
  • Local script should be attached to some game object (npc / player / item / etc). It works only while the object it is attached to is in a loaded cell. It can register only handlers related to the object, but doesn't have such limitations as global scripts and by its functionality is not worse than MWSE. For example AI packages can be implemented as local scripts. Local scripts can produce events to be handled by a global script.
In multiplayer mode global scripts will be server-side and local scripts will be client-side. When a global script starts a local script, it will be evaluated on a client that manages the object it is attached to.

On every scripting iteration (for performance reasons we can make it different from 1 iteration per frame) OpenMW should:
  1. [client side] Check if there are timer events or any events from locally controlled actors/items.
  2. [client side] If there are any events call the corresponding handlers in local scripts.
  3. [client side] Send events (including ones created by local scripts), updated coordinates and states of the controlled objects to the server.
  4. [server side] Gather events from all the clients. Call handlers, registered by global scripts.
  5. [server side] Send updated data back to clients.
In singleplayer steps 3 and 5 are skipped and all others should be done locally.

In order to make every single player mod compatible with multiplayer we also should by design guarantee that:
  • When a player disconnects, the internal state of all local scripts is stored on the server.
  • Local scripts API always takes into account that there can be several players. I.e. if any function does something with a player it accepts player id as an argument.
The most problematic in this approach is the same problem of conversion between “OpenMW data model” and “TES3MP-server data model”. Local scripts API should be consistent with global scripts API. And ideally in the single player case there should be no data model conversion at all. But it requires significant changes on both sides of OpenMW and TES3MP.

Work plan

Here is a very rough proposal about the plan, as discussed and agreed with TES3MP's developer David Cernat:
  1. We need to work from both sides of OpenMW and TES3MP simultaneously, so it is very important to keep TES3MP up to date with OpenMW.
    1. (TES3MP) Rebase to the actual version of OpenMW. It is quite challenging since OpenMW changes quickly.
    2. (OpenMW) To simplify the process we can minimize the diff between projects by merging from TES3MP to OpenMW some functions that make sense not only for multiplayer (like World::setYear). Later we will make the functions accessible via scripting.
  2. Maybe switch the Lua binding library from LuaBridge to something more convenient (sol2, sol3). Also consider the possibility to use Lua C API directly if it allows to reduce the number of dependencies. Separate our Lua wrappers from the scripting API, place it to “components/lua” (currently it is “apps/openmw-mp/script”), and merge the “components/lua” to OpenMW. At this point OpenMW will require Lua to build, but will not actually use it yet.
  3. (TES3MP) Get rid of the separate "server" app in TES3MP. Move the server code to a new folder in the client app. Thus, a "server" is simply a "client" that isn't actively loading physics/pathfinding/graphics/etc. for cells, but has access to all cell contents and gameplay variables. At this point “server” should use the same data model as “client”. It will significantly change the API of TES3MP scripts since scripts currently have direct access to the data. Most probably it will also require some changes in the “OpenMW data model”. Such changes should be immediately pushed to OpenMW in order to prevent future merging problems.
  4. (OpenMW) In parallel with the previous step, introduce the basic framework for implementing local Lua scripts. It is better to do it on the OpenMW side because otherwise we will face serious merging problems later.
  5. Merge implementation of global scripts from TES3MP to OpenMW. At this point the scripting system will be in the same state in both projects. All further changes should be first committed to OpenMW and will come to TES3MP after the next rebase.
  6. Make more functions available through Lua scripting, carefully deciding which ones are more appropriate for local or global scripting and which ones should be available in both.
  7. Replace current implementation of mwscript with a tool that converts it to Lua scripts.
  8. At this point we already have some dehardcoding, but can finally focus on it in earnest.
Update: jump here to see the current status of OpenMW Lua.
Last edited by ptmikheev on 02 Aug 2021, 18:20, edited 1 time in total.
User avatar
akortunov
Posts: 899
Joined: 13 Mar 2017, 13:49
Location: Samara, Russian Federation

Re: Lua scripting in OpenMW

Post by akortunov »

ptmikheev wrote: 17 Oct 2020, 05:30 but can not do things that require game assets (pathfinding, raytracing, direct AI control and so on).
And we should discuss how to workaround such limitations via "combined" scripting systtem. For example, if GUI widgets are stored on client and we are going to have a client-side scripting, it would be simpler to tweak them via client-side scripting rather than implement a bulk Xorg-like framework to send UI to server and back (which will be an additional source of bugs and limitations). The same thing for scene graph scripting and so on.
ptmikheev wrote: 17 Oct 2020, 05:30 In multiplayer mode global scripts will be server-side and local scripts will be client-side.
I am not sure if it is a good idea. From my inderstanding, an only difference between local and global script is that local script instance works only with one object. Also client-specific things (GUI, input bindings, scene graph API) logically should work on client side, while things that affect game world (and all clients) logically belong to the server, and it does not really matter if script has an object instance or it is not. But probably you know better, though.
ptmikheev
Posts: 69
Joined: 01 Jun 2020, 21:05
Gitlab profile: https://gitlab.com/ptmikheev

Re: Lua scripting in OpenMW

Post by ptmikheev »

Also client-specific things (GUI, input bindings, scene graph API) logically should work on client side.
It can be a local script, attached to the player character. It is quite logical because the input bindings makes sense only for this specific player and shouldn't directly affect other objects.
while things that affect game world (and all clients) logically belong to the server
Currently in TES3MP all game mechanics are processed locally (i.e. each client controls some subset of NPCs and sends updates to others) and synchronized by server. It has disadvantages like a possibility of cheating, but the great benefit is that the server is simple, can handle many players across a huge world, and doesn't require a supercomputer for it. It makes perfect sense to run a script attached to some actor on the same client that controls this actor.
And we should discuss how to workaround such limitations via "combined" scripting systtem. For example, if GUI widgets are stored on client and we are going to have a client-side scripting, it would be simpler to tweak them via client-side scripting rather than implement a bulk Xorg-like framework to send UI to server and back (which will be an additional source of bugs and limitations). The same thing for scene graph scripting and so on.
There are different ways. I personally like this one:
GUI is controlled by a local script, attached to the player character. All low level stuff (like placing specific widgets) is always done locally. If something affects multiplayer (for example we initiate a barter dialogue with some other player), the local script produces a custom high-level event (that for example can hold a list of goods for barter). This event will be then processed by an event handler in a corresponding global script.
User avatar
urm
Posts: 83
Joined: 02 Jun 2017, 16:05
Gitlab profile: https://gitlab.com/uramer

Re: Lua scripting in OpenMW

Post by urm »

It is worth mentioning that there is a pull request that adds custom ui support to TES3MP. It is quite big and can be an example of a “complicated workaround”.
I'd argue my PR is not a "workaround". Most of its code has to do with neither OpenMW or TES3MP, but with MyGUI. I'd probably approach it very similarly if I was making a UI system to be used by the client-side OpenMW Lua scripts.
Last edited by urm on 17 Oct 2020, 23:51, edited 1 time in total.
ptmikheev
Posts: 69
Joined: 01 Jun 2020, 21:05
Gitlab profile: https://gitlab.com/ptmikheev

Re: Lua scripting in OpenMW

Post by ptmikheev »

urm wrote: 17 Oct 2020, 23:07
It is worth mentioning that there is a pull request that adds custom ui support to TES3MP. It is quite big and can be an example of a “complicated workaround”.
I'd argue my PR is not a "workaround". Most of its code has nothing to do with either OpenMW or TES3MP, but with MyGUI. I'd probably approach it very similarly if I was making a UI system to be used by the client-side OpenMW Lua scripts.
I will not argue. I haven't looked at the MR in detail. Only saw that it is quite big. Sorry. I can remove the sentence if you want.
User avatar
urm
Posts: 83
Joined: 02 Jun 2017, 16:05
Gitlab profile: https://gitlab.com/uramer

Re: Lua scripting in OpenMW

Post by urm »

It will significantly change the API of TES3MP scripts since scripts currently have direct access to the data.
One thing to note here is how server data is stored. Currently in 0.7 it's just json files, however I have a PR with multi-threaded postgres support. In my opinion, it's important to at least have an option of supporting large servers (over 100 players), and using a database of some sort is a pretty obvious first step. That can also open up new features to the scripts, e.g. storing ESP information in a database can allow complex searches through them in real time.

Obviously, we don't want singleplayer/cooperative users to install, say, postgres, so in this case we should fall back to raw files or something like sqlite.

I know at some point there were talks about making OpenMW entirely server-based. So even while playing singleplayer, you'd essentially be playing multiplayer with only 1 player connected. Is that approach abandoned?

Maybe OpenMW save files could be just an sqlite database then? (still a single file) That would allow us to unify server data state and client data state storage formats.

If we are using different databases in different use cases, we will need an abstraction layer to access them. On Lua side, we will have to implement it ourselves, but on C++ side we can use an existing library (just to give an example, this looks good at the first glance https://www.codesynthesis.com/products/odb/doc.xhtml)
User avatar
psi29a
Posts: 5355
Joined: 29 Sep 2011, 10:13
Location: Belgium
Gitlab profile: https://gitlab.com/psi29a/
Contact:

Re: Lua scripting in OpenMW

Post by psi29a »

This is something that I've discussed with David in the past. He seemed to be partial to MongoDB because of how JSON like documents are.

I suggested sqlite3.

To best honest, I don't like adding yet another dependency, especially a rdbms or equivalent. If this could be optional, great as I suspect that a great many people just want to run openmw as single player.
User avatar
urm
Posts: 83
Joined: 02 Jun 2017, 16:05
Gitlab profile: https://gitlab.com/uramer

Re: Lua scripting in OpenMW

Post by urm »

psi29a wrote: 18 Oct 2020, 00:10 This is something that I've discussed with David in the past. He seemed to be partial to MongoDB because of how JSON like documents are.

I suggested sqlite3.

To best honest, I don't like adding yet another dependency, especially a rdbms or equivalent. If this could be optional, great as I suspect that a great many people just want to run openmw as single player.
Yes, some noSQL / document-based DB was the first thing I looked into, but since I was limited to Lua libraries, there were no acceptable options for them. Although we don't really need any noSQL features, so just storing json as text in sqlite/postgres (both have some limited ways of querying such json btw) wasn't really any worse.
I agree that minimizing dependencies would be great. However the only alternative I see is having them as an optional Lua "mod" instead. There is no reasonable database abstraction layer available for Lua, and even just getting a basic sqlite/postgres involves building multiple dependencies. So the optimal approach in that case might be just using a C/C++ library and creating a Lua binding for it ourselves.
One issue I see with getting rid of this dependency in such a way, is that we wouldn't have an easy way of unifying storage formats between singleplayer and multiplayer then, unless we essentially make a database abstraction layer ourselves (strictly worse than using an existing GPL library IMO).

Speaking of which, another topic to discuss about Lua mods (particularly client-side ones) is what we should do with C Lua libraries (dlls), and security in general. E. g. giving Lua mods full access to the file system might be unwise.
ptmikheev
Posts: 69
Joined: 01 Jun 2020, 21:05
Gitlab profile: https://gitlab.com/ptmikheev

Re: Lua scripting in OpenMW

Post by ptmikheev »

I know at some point there were talks about making OpenMW entirely server-based. So even while playing singleplayer, you'd essentially be playing multiplayer with only 1 player connected. Is that approach abandoned?
My arguments against this approach are pretty similar to what I have written in the section "A" of the starting post.
It has big performance cost and (if we don't introduce local scripts) significantly complicates the protocol.
Instead we now want to make server data layer uniform with the client. Then in case of singleplayer there will be no additional overhead.
Maybe OpenMW save files could be just an sqlite database then? (still a single file) That would allow us to unify server data state and client data state storage formats.
I very like the idea of unifying storage formats. Ideally singleplayer saves and multiplayer saves should be the same.
I am not very familiar with the formats, so maybe the question is stupid. Is it possible to go the opposite way? If we make the server data layer uniform with the client (i.e. step 3 of the plan), then will it be possible to use the current OpenMW save format in multiplayer as well?
If we are using different databases in different use cases, we will need an abstraction layer to access them. On Lua side, we will have to implement it ourselves, but on C++ side we can use an existing library
Are there any reasons to have I/O on the Lua side at all? I think that C++ code can just store all internal variables of all scripts within the save file, so for Lua saving and loading can be completely transparent. Do I miss something?
Speaking of which, another topic to discuss about Lua mods (particularly client-side ones) is what we should do with C Lua libraries (dlls), and security in general. E. g. giving Lua mods full access to the file system might be unwise.
Yes, it is an important question. And not only about security, but also about compatibility between different platforms. I don't have any concrete opinion about it yet.
User avatar
urm
Posts: 83
Joined: 02 Jun 2017, 16:05
Gitlab profile: https://gitlab.com/uramer

Re: Lua scripting in OpenMW

Post by urm »

ptmikheev wrote: 18 Oct 2020, 13:49 My arguments against this approach are pretty similar to what I have written in the section "A" of the starting post.
It has big performance cost and (if we don't introduce local scripts) significantly complicates the protocol.
Instead we now want to make server data layer uniform with the client. Then in case of singleplayer there will be no additional overhead.
Obviously, we would still have local scripts. There is no reason not to, to be honest. Current tes3mp server is extremely lightweight, and performs well even despite having many extremely inefficient parts. So I don't think there would be a meaningful performance difference. I would say the main argument for not having any networking for singleplayer, is the theoretical possibility for inconsistent behavior even with 0 ping.
I very like the idea of unifying storage formats. Ideally singleplayer saves and multiplayer saves should be the same.
I am not very familiar with the formats, so maybe the question is stupid. Is it possible to go the opposite way? If we make the server data layer uniform with the client (i.e. step 3 of the plan), then will it be possible to use the current OpenMW save format in multiplayer as well?
The problem is that they are designed for very different use cases. Game saves are supposed to be as small and efficient as possible, and don't need to be written/read in real time. The server, on the other hand, regularly loads/unloads new data from/to disk, so it needs a storage format that's optimized for speed, rather than size.
Are there any reasons to have I/O on the Lua side at all? I think that C++ code can just store all internal variables of all scripts within the save file, so for Lua saving and loading can be completely transparent. Do I miss something?
Well, one reason to have I/O on Lua side is that tes3mp already has it there. The other is that if we want to minimize dependencies for single/coop use, more advanced and efficient storage options would have to come as Lua modules (still using C dlls likely).
I'm not sure how you see that automatic saving and loading to be honest. You certainly don't want to sync every script variable automatically (I don't think you can anyway), as they might depend on some non-permanent factors, such as currently connected players or even the current time. You would still need some API to save/load data from the Lua side, and some way to initialize scripts.
Yes, it is an important question. And not only about security, but also about compatibility between different platforms. I don't have any concrete opinion about it yet.
If we allow Lua mods to have dlls (I'd say we should), we have to at least warn players about potential safety issues. With platform compatibility, we could either have every mod provide dlls for multiple platforms, or attempt to build them during installation. There are two existing packet systems for Lua: LuaRocks and LuaDist, the former goes with the build approach, the latter supports both.
There should probably also be some sandboxing for the actual Lua code. For example, I don't see why we would give direct access to the file system, we should probably rely on a custom storage API instead.
Post Reply