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
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)
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)
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”.
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.
On every scripting iteration (for performance reasons we can make it different from 1 iteration per frame) OpenMW should:
- [client side] Check if there are timer events or any events from locally controlled actors/items.
- [client side] If there are any events call the corresponding handlers in local scripts.
- [client side] Send events (including ones created by local scripts), updated coordinates and states of the controlled objects to the server.
- [server side] Gather events from all the clients. Call handlers, registered by global scripts.
- [server side] Send updated data back to clients.
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.
Work plan
Here is a very rough proposal about the plan, as discussed and agreed with TES3MP's developer David Cernat:
- 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.
- (TES3MP) Rebase to the actual version of OpenMW. It is quite challenging since OpenMW changes quickly.
- (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.
- 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.
- (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.
- (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.
- 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.
- 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.
- Replace current implementation of mwscript with a tool that converts it to Lua scripts.
- At this point we already have some dehardcoding, but can finally focus on it in earnest.