version from 06.04.2025, written by AgentSmith

# How to use WSE/LUA

This is the documentation for the Lua scripting system of WSE, a powerful alternative to regular modsys scripts.
LUA will run on top of module system code. You could code almost an entire module without touching the modsys,
or add features to modules even if you don't have the source code.
This guide is only a documentation of the provided API, to learn Lua itself there exist many tutorials online.
At the end of this file there is some example code that can help you get started.

##
BASICS

### Why use Lua?

- Way nicer syntax, datastructures (no more troop slots for arrays), real functions with return values, easy string handling, etc.
- No compilation needed, no python needed
- Hot reloading *
  Lua is a dynamic scripting language, so you can execute code from disk at any time with dofile().
  A dofile("main.lua") is enough to test your changes in many cases.
  It isn't equivalent to a full restart though, because the lua state will not get reset, only overwritten. Any triggers you added or any other changed game state remain too. This must be managed manually.
  (don't need to restart to test changes)
- Debugger available (lets you step through your code line by line)
- Better performance
- Multithreading (luaLanes library)
- Use any Lua code from the internet
- No confusing fixed point multiplier, easy (vector) math

      local area = math.pi * r^2

  instead of

      							(store_mul, ":area", ":r", ":r"),
      							(val_mul, ":area", 314159),
      							(val_div, ":area", 100000), # *(314159/100000)=*PI

### Setup

- Install WSE as usual, make sure lua51.dll is in the same folder as mb_warband.exe
- Create [warband_dir]\Modules\[module_name]\lua\, e.g. C:\Program Files (x86)\Steam\steamapps\common\MountBlade Warband\Modules\Native\lua\
- Create main.lua inside it. This file, if it exists, gets loaded and executed by WSE at startup, before any of the module system code. You don't need any compiler.
  Typically, here you load other files, run some init code, and/or register triggers.
  In normal modsys, for something to happen you would for example write a trigger in module_mission_templates.py.
  It's the same in lua, but you register the trigger at runtime in main.lua (or load a file that does it).

- Create [warband_dir]\Modules\[module_name]\lua\msfiles\ (msfiles - "Module System files")
- Copy header_operations.py to your msfiles folder. This is so that WSE can translate operation names to opcodes and is neccessary for calling module system operations from lua.
  It's a good idea to start the game now and see if there any errors parsing this file. Delete any bad lines, but make sure any opcodes you need & the lists at the end (lhs_operations = [...], etc) are intact.

WSE/LUA uses a modified version of LuaJIT 2.1.0, which is based on and ABI compatible to Lua 5.1

Lua Reference Manual
Programming in Lua (this is for Lua 5.0 while WSE has 5.1, but the differences are small)
Lua IDE for testing code snippets and debugging

##
THE "GAME" TABLE

The game table is a predefined global table that allows you access to the game from lua.

Operations

To call operations, use game.operation_name(arg1, arg2, ...)

Example: game.display_message("test string")

String and pos arguments are passed to the game via the games registers (overwritting them), starting from reg[128] and counting downwards.

Notice that the argument type matters. For example, if an operation expects an integer, you cannot pass a string that contains a number.

Return values are:

    						boolDidNotFail, intError						= game.cf_operation(arg1, arg2, ...) for can_fail operations
    						intResult, intError									= game.lhs_operation(lhs_arg, arg1, arg2, ...) for lhs operations
    						boolDidNotFail, intResult, intError	= game.cf_lhs_operation(lhs_arg, arg1, arg2, ...) for operations which are both can_fail and lhs
    						intError														= game.operation(arg1, arg2, ...) for all other operations

    						(lhs = left hand side? Operations that assign a value, e.g. val_add)
    						(cf  = can fail, e.g. agent_is_active)

Note that in Lua, you don't have to assign all return values. For example, if you don't care about the error code, just omit the corresponding lhs var.

I'm not sure what the error code signalizes, however it should be 0 if there was no error.

With Lhs operations, there is a slight problem. Their syntax is:
(lhs_op, <destination_param>, <param1>, <param2>, ...)

While in lua you want to write:
result = game.lhs_op(param1, param2, ...)

Why is this a problem?
Because of different parameter count. Now, WSE could detect that you are calling an Lhs operation and fill in the missing parameter, but: the operation might actually use the initial value of it to affect the result.
An obvious example for this is val_add. It makes no sense to write result = val_add(5). What is it adding 5 to?

Whether or not an operation uses the initial value can't simply be detected by WSE. So in the past it used to be that you had to write result = game.lhs_op(some_number, var1, var2, ...) for all lhs operations (some_number was just 0 mostly). Annoying.

However, after further inspection, it is reasonable to assume that only val_ operations do this. Therefore WSE update adds game.op:

    						local val = game.agent_get_slot(0, agent_id, slot_no)
    						local val = game.op.agent_get_slot(agent_id, slot_no)

It will add the 0 automatically for any lhs_operation, except for ones that start with val_. If you find operations where this is wrong, please contact AgentSmith on TaleWorlds forum.

If you don't want to write the .op all the time, call game.op.make_default(). It is optional because of legacy code.
gop = game.op also works.

Registers, Global Variables

To access registers, use game.reg[n] , game.sreg[n] and game.preg[n]

Example:

    						game.reg[0] = 123
    						local i = game.reg[0]
    						game.sreg[0] = "test string"
    						local s = game.sreg[0]
    						game.preg[0] = game.pos.new()
    						local pos = game.preg[0]

As of the current version, registers are copied by value instead of by reference. So doing things like:

    						game.preg[0]:rotX(30)

Does not change the actual game register. This is likely going to be improved in a future version. What you have to do instead for now is:

    						local p = game.preg[0]
    						p:rotX(30)
    						game.preg[0] = p

To access global variables, use game.gvar.gvar_name

    						game.gvar.g_encountered_party = 123
    						print(game.gvar.g_encountered_party)

WSE will read variables.txt at startup to translate gvar_name. If this file is incorrect for any reason, accessing a gvar may produce an error or access a random gvar. You can't create new gvars.

Constants

For every header_[name].py or ID_[name].py file you copied to your "msfiles" folder, a new table will be generated: game.const.[name]
This table will be filled with all the constants from that file.
If there are two files with the same [name], e.g. header_scenes and ID_scenes, the tables will get merged.

As of the current version, only lines that match one of the two following regexes (regular expressions) will be accepted:

    						^(\w+)=(((-)?0x[\da-fA-F]+)|((-)?\d+(\.\d+)?))$
    						^(\w+)=(\w+)$

This probably looks intimidating to you, but really all it says is that your constant must either be a number, a hex number, or another constant (from the same file, currently).

In other words: constant = num|hexNum|otherConstant

If the match fails, the line will be ignored. To display a warning in this case, set [lua] disable_game_const_warnings = 0 in wse_settings.ini.

You can omit the [name] part in game.const.[name].[constant], in which case all tables in game.const will be searched for your constant and the first result will be used.

So let's say you want to add a deathmatch trigger for ti_on_agent_hit. Instead of this:

    						game.addTrigger("mst_multiplayer_dm", -28, 0, 0, condCB)

You could copy header_triggers.py to your msfiles folder and do:

    						game.addTrigger("mst_multiplayer_dm", game.const.ti_on_agent_hit, 0, 0, condCB)
    						--or if you like to be specific:
    						game.addTrigger("mst_multiplayer_dm", game.const.triggers.ti_on_agent_hit, 0, 0, condCB)

Triggers

To add mission template triggers, use index = game.addTrigger(strTemplateId|intTemplateNo, numCheckTime, numDelayTime, numRearmTime, funcConditionsCallback, [funcConsequencesCallback])

These work exactly like module system triggers. Times can be float values or values from header_triggers.py (the numbers, string names are not supported yet), just like you would do it in the MS. If conditionsCallback returns false, consequencesCallback will not be executed. If it returns true or nothing, consequencesCallback will be executed.

Example:

    						function condCB()
    							doStuff()
    							return true
    						end
    						game.addTrigger("mst_multiplayer_dm", 1, 0, 0, condCB)

addTrigger returns an integer that is the index of the added trigger.

You can also remove triggers by using game.removeTrigger(strTemplateId|intTemplateNo, intTriggerIndex). If intTriggerIndex is negative, the trigger with (numTriggers + intTriggerIndex) gets removed (e.g. to remove the last trigger, use -1). Returns success.

Useful functions:

- game.getCurTemplateNo() returns the index of the current mission template. Note that no template will be loaded at the time main.lua is executed.
- game.getCurTemplateId() returns the id/name of the current mission template. Note that no template will be loaded at the time main.lua is executed.
- game.getNumTemplates() returns number of mission templates.
- game.getTemplateId(index) returns the id/name of mission template at index.
- game.getNumTriggers(strTemplateId|intTemplateNo) returns the number of triggers for this mission template.

Item triggers:

- index = game.addItemTrigger(strItemID|intItemNo, numTriggerInterval, funcCallback)
- success = game.removeItemTrigger(strItemID|intItemNo, index)
- num = game.getNumItemTriggers(strItemID|intItemNo)

Example:

    						game.addItemTrigger("itm_fighting_pick", game.const.ti_on_weapon_attack, function()
    							print("agent no " .. game.store_trigger_param_1(0) .. " swung a fighting pick")
    						end)

Scene prop triggers:

- index = game.addScenePropTrigger(strPropID|intPropNo, numTriggerInterval, funcCallback)
- success = game.removeScenePropTrigger(strPropID|intPropNo, index)
- num = game.getNumScenePropTriggers(strPropID|intPropNo)

Example:

    						game.addScenePropTrigger(game.const.spr_apple_a, game.const.ti_on_scene_prop_init, function()
    							print("Apple " .. game.store_trigger_param_1(0) .. " initialized.")
    						end)

World triggers:

Due to technical reasons, these work differently. You need to assign a callback to game.OnWorldTrigger:

    game.OnWorldTrigger = function(date) ... end

It will be called after the game checks the modsys triggers, and receive time in hours. It will be called even if the game is paused, but date won't change.
The game will save the state of its triggers to the savefile, so that for example ti_once does not reactivate when you load a save. You have to save this data manually if you need it. (Use game.OnSave(), game.OnLoadSave())
Time conversions according to WSE source:

- hours = date
- days = date / 24
- weeks = date / 168
- months = date / 720
- years = date / 8640

For technical reasons, if there are modsys errors in a trigger added in lua (for example invalid agent_id), it will report as line 0, opcode 5113.

Iterators

To use iterators (try_for_agents, try_for_players, ...), use the provided iterator functions game.partiesIt, game.agentsIt, game.propInstIt, game.playersIt

These work exactly like module system iterators and take the same arguments.

    						game.partiesIt()
    						game.agentsIt([pos, radius, [use_grid]]) --pos: integer for pos register or table of type game.pos
    						game.propInstIt([subKindNo], [metaType]) --a.k.a. [object_id], [object_type]
    						game.playersIt([skip_server])

Example:

    						for curPlayer in game.playersIt(true) do --skip server. You can also use 1 or 0
    							foo(curPlayer)
    						end

Positions

To work with positions, the following "classes" are provided: game.rotation, game.pos and vector3 (see under miscellaneous)

Keep in mind that these don't need the fixed point multiplier that the module system requires you to use.

The MS multiplier is essentially a way of specifying the current unit of length.

    						set_fixed_point_multiplier(1),
    						position_get_x(":x", 0), #get pos0_x in meters
    						set_fixed_point_multiplier(100),
    						position_get_x(":x", 0), #get pos0_x in centimeters

In Lua it's simpler, you work with floating point numbers. "1.0" always means 1 meter, "0.01" always means 1 centimeter.

    						--this is equivalent to above modsys example
    						print(game.preg[0].o.x) --pos0_x float

- game.rotation

  - game.rotation.new([obj]) - constructor. obj can be used to specify initial values.

  - game.rotation.prototype

        										s = vector3.new({x = 1}) --x axis
        										f = vector3.new({y = 1}) --forwards/y axis
        										u = vector3.new({z = 1})  --up/z axis
        										function getRot(self) --returns vector3.new({z = yaw, x = pitch, y = roll})
        										function rotX(self, angle) --rotate around x axis, angle in degrees
        										function rotY(self, angle) --rotate around y axis, angle in degrees
        										function rotZ(self, angle) --rotate around z axis, angle in degrees
        										function rotate(self, rotVec3) --rotate around all axis, zxy order, angle in degrees

- game.pos

  - game.pos.new([obj]) - constructor. obj can be used to specify initial values.

  - game.pos.prototype

        										o = vector3.new() --position in the world
        										rot = game.rotation.new() --rotation, or heading of the pos if you like

        										--see game.rotation.prototype for these
        										function getRot(self)
        										function rotX(self, angle)
        										function rotY(self, angle)
        										function rotZ(self, angle)
        										function rotate(self, rotVec3)
        										function moveX(self, val) --Move by val into local X direction
        										function moveY(self, val) --Move by val into local Y direction
        										function moveZ(self, val) --Move by val into local Z direction
        										function move(self, val) --Move by val into local directions, val must be table with any of x,y,z entries
        										function dist(self, pos2) --returns distance between self.o and pos2.o
        										function isBehind(self, pos2) --returns true if self is behind pos2 (shorthand for game.position_is_behind_position)

Example:

    						local pos0 = game.preg[0]
    						local newRot = game.rotation.new(pos0.rot)
    						local newPos = game.pos.new({rot = newRot})
    						newPos:rotX(45)
    						newPos:rotate({x = 90, z = 180})
    						newPos:move({x = 10, z = 5})

Presentations

You can add presentations using index = game.addPrsnt(tablePrsnt)

tablePrsnt must have the following format

    						tablePrsnt = {
    							id = str,
    							[flags] = {int, int, ...},
    							[mesh] = int,
    							triggers = {[numTriggerConst] = func, ...}
    						}

[flags] can be omited and defaults to no flags at all. If you copied header_presentation.py to your msfiles folder, you can use the flags defined there.

[mesh] can be omited and defaults to 0. If you copied ID_meshes.py to your msfiles folder, you can use the mesh nos defined there.

triggers must have at least one element, the key being a number and the value being a function. If you copied header_triggers.py to your msfiles folder, you can use the trigger values defined there.

Return value: the index of the new presentation.

Example:

    						--copy header_presentations.py, header_triggers.py and ID_meshes.py to your msfiles folder for this example.
    						local index = game.addPrsnt({
    							id = "myPrsnt",
    							flags = {game.const.prsntf_read_only, game.const.prsntf_manual_end_only},
    							--you can initialize an array like this, without keys - they don't matter here anyway.
    							mesh = game.const.mesh_cb_ui_main,
    							triggers = {
    									[game.const.ti_on_presentation_load] = function ()
    										--The const inside the [] declares a number key, similar to keyName = 123 for string keys.
    										game.presentation_set_duration(9999999)
    										local overlay = game.create_mesh_overlay(0, game.const.mesh_mp_ingame_menu)

    										local position = game.pos.new()
    										position.o.x = 0.3
    										position.o.y = 0.3
    										game.overlay_set_position(overlay, position)

    										position.o.x = 0.5
    										position.o.y = 0.8
    										game.overlay_set_size(overlay, position)
    									end
    							}
    						})
    						game.start_presentation(index)

Remove using game.removePrsnt(index). Remember, removing will shift above indices.

On dedicated, add/remove will do and return nothing.

For technical reasons, if there are modsys errors in a presentation added in lua (for example invalid agent_id), it will report as line 0, opcode 5113.

Particle Systems

You can add particle systems using index = game.addPsys(tablePsys)

tablePsys must have the following format

    						tablePsys = {
    							id = str, [flags] = {int, int, ...}, mesh = str|int,
    							num_particles = int, life = num, damping = num, gravity_strength = num, turbulance_size = num, turbulance_strength = num,
    							alpha_keys			= {{num, num}, {num, num}},
    							  red_keys			= {{num, num}, {num, num}},
    							green_keys			= {{num, num}, {num, num}},
    							 blue_keys			= {{num, num}, {num, num}},
    							scale_keys			= {{num, num}, {num, num}},
    							emit_box_size		= {num, num, num},
    							emit_velocity		= {num, num, num},
    							emit_dir_randomness = num,
    							rotation_speed = num,
    							rotation_damping = num
    						}

[flags] can be omited and defaults to no flags at all.

Return value: the index of the new particle system.

Example:

    							local psys = game.addPsys({id = "blabla", flags = {game.const.psf_billboard_3d, game.const.psf_global_emit_dir}, mesh = "prt_mesh_snow_fall_1",
    								num_particles = 150,
    								life = 2,
    								damping = 0.2,
    								gravity_strength = 0.1,
    								turbulance_size = 30,
    								turbulance_strength = 20,
    								alpha_keys = {{0.2, 1}, {1, 1}},
    								red_keys = {{1.0, 1.0}, {1, 1.0}},
    								green_keys = {{1.0, 1.0}, {1, 1.0}},
    								blue_keys = {{1.0, 1.0}, {1, 1.0}},
    								scale_keys = {{1.0, 1.0},   {1.0, 1.0}},
    								emit_box_size = {10, 10, 0.5},
    								emit_velocity = {0, 0, -5.0},
    								emit_dir_randomness = 1,
    								rotation_speed = 200,
    								rotation_damping = 0.5
    							})
    							game.particle_system_burst(psys, game.preg[0], 20)

Remove using game.removePsys(index). Remember, removing will shift above indices.

On dedicated, add/remove will do and return nothing.

Module operation hooking

You can hook module operations using game.hookOperation(operation, funcCallback).

This means whenever the module system uses the operation, your callback will be executed first.

operation must be either the operation name as a string or an opcode integer.

Operation hooks are experimental - it can be very useful, but you risk crashes.

The return values of funcCallback control further behaviour.

If it returns nothing or true, the module operation will be executed as usual.

If the first return value is false, the module operation will not be executed.

If the second return value is a bool, the module system will think it is executing a cf operation (so if the bool is false, it will break execution, like e.g. (eq, 1, 0),).

If the second return value is a number, the module system will think it is executing a lhs operation and try to set the return value

If the callback returns three values, the module system will think it is executing a cf/lhs operation. The 2nd return value must be the cf value and the 3rd the lhs value

In short, the variants are:

    						boolExecute
    						boolExecute, boolFail
    						boolExecute, numLhs
    						boolExecute, boolFail, numLhs

To unhook an operation, use game.hookOperation(operation, nil).

The hook will not be triggered when calling operations from lua. However, this task can be achieved by overwritting the game metatable, which is defined as:

    						game.mt = {
    								__index = function(table, key)
    										return function(...)
    												return game.execOperation(key,...)
    										end
    								end
    						}

One could, for example, do (NOT FINISHED YET):

    						game.mt = {
    								__index = function(table, key)
    										If key == "player_get_gold" then
    												return myCustomGoldCalculation
    												--return function that takes all args (...) and returns gold
    												--this function should, if neccessary, use game.execOperation("player_get_gold", args)
    												--instead of game.player_get_gold(args), for avoiding an infinite loop.
    										end
    										return function(...)
    												return game.execOperation(key,...)
    										end
    								end
    						}
    						setmetatable(game, game.mt)

You could even go one step further and create a wrapper for game.hookOperation that automates this process for you.

Script hooking

You can hook scripts using game.hookScript(script_no, funcCallback).

funcCallback will receive all script parameters. Its return values control further behaviour:

If it returns false, the script will not be executed.

If it returns numbers, the script parameters will be replaced with them, then the script is executed.

Example:

    						local a = 0
    						game.hookScript(game.script.game_get_console_command, function(...)
    						  print("Hook")
    						  a = a + 1

    						  if a <= 5 then
    						    local s = ""
    						    for i = 1, select("#", ...) do
    						      s = s .. tostring(select(i, ...)) .. "     "
    						    end

    						    print("CONSOLE CMD", a, "blocked, params were: ", s)
    						    return false

    						  elseif a <= 10 then
    						    return 2, 1, 4 --set team 1 bot count to 4. At least for NW

    						  elseif a == 15 then
    						    game.hookScript(game.script.game_get_console_command, nil)
    						  end
    						end)

To unhook a script, use game.hookScript(script_no, nil).

Other functions

- game.getScriptNo(script_name) use in conjunction with game.call_script(scriptNo, ...)

  Example:

      									local no = game.getScriptNo("game_start")
      									game.call_script(no)

  WSE Update now provides a wrapper: game.script.[name]

      										game.call_script(game.script.game_start)

- game.getCurTemplateNo() returns the index of the current mission template. Note that no template will be loaded at the time main.lua is executed.
- game.getCurTemplateId() returns the id/name of the current mission template. Note that no template will be loaded at the time main.lua is executed.
- game.getNumTemplates() returns number of mission templates.
- game.getTemplateId(index) returns the id/name of mission template at index.
- game.getNumTriggers(strTemplateId|intTemplateNo) returns the number of triggers for this mission template.
- game.getMeshId(index) returns name of mesh at index (mesh no). Note: This does not access the mesh list in module_meshes. Index is for the internal list of all meshes loaded from the .brf files.
- game.getNumMeshes() returns number of meshes. Note: This does not access the mesh list in module_meshes. It is for the internal list of all meshes loaded from the .brf files.
- game.OnRglLogWrite(str) this function, if it exists, gets called when something gets written to rgl_log.txt. It receives the log message as its parameter. A bad idea is to use game.display_message in this function, as it will easily cause an infinite loop. You can however use print(). Any error (which would normally trigger the event again, which would trigger the error, ...) caused by this function will be catched and logged safely.

- game.OnChatMessageReceived(intPlayerNo, boolIsTeamChat, strMsg) this function, if it exists, gets called when a chat message is received (both client and server). It will be called after the corresponding module system script. If it returns a string the message will be overwritten, if it returns true the message will be supressed. Returning something will overwrite any action the module script may have taken.

- game.OnGameLoad() this function, if it exists, gets called when the game loads.

  game.OnSave() this function, if it exists, gets called after a game is saved successfully.

  game.OnLoadSave() this function, if it exists, gets called after a savegame is loaded successfully.

- game.fail() when used during a lua_call operation it will make the MS code fail, like any other cf operation can do.

  Example:

      								(try_begin),
      										(lua_push_int, 456),
      										(lua_call, "@is123", 1),
      										(display_message, "@Yes it is."),
      								(else_try),
      										(display_message, "@Nah"),
      								(try_end),

      								function is123(val)
      										if val ~= 123 then
      												game.fail()
      										end
      								end

Other WSE LUA API functions - these could be useful for advanced users, but you should get along fine without ever using them.

- game.execOperation(strOperationName, arg1, arg2, ....) - see operations
- game.getOperationFlags(strOperationName) - Flags: None = 0x0, Lhs = 0x1, Cf = 0x2 (this is what WSE figured from reading header_operations.py, the engine may not agree).
- game.getReg(intTypeId, intIndex) typeIds are: 0 = int, 1 = str, 2 = pos
- game.setReg(intTypeId, intIndex, val)
- game.getGvar(key)
- game.setGvar(key, value)

##
MISCELLANEOUS

Globals

- tableShallowCopy(t, copyMetatable) - returns a copy of table t, however any object items will still reference the same object. If copyMetatable ~= nil, the returned table will have the same metatable as t, as given by getmetatable(t).
- tableRecursiveCopy(t, copyMetatable) - returns a copy of table t. All table items in t and further down the "tree" will be actual copies by value instead of by reference. If copyMetatable ~= nil, all metatables will be copied (by value), as given by getmetatable(t).
- vector3 - class for storing and manipulating 3D vectors

  - vector3.prototype

        										x = 0
        										y = 0
        										z = 0
        										--You can also use vec3[1], vec3[2], vec3[3] instead of x,y,z
        										function len(self) --returns the length of the vector
        										function dist(self, vec2) --returns the distance between self and vec2
        										function dot(self, vec2) --returns the dot product of self and vec2
        										function cross(self, vec2) --returns the cross product of self and vec2
        										function unit(self) --returns a normalized copy of itself
        										function lerp(self, goal, alpha) --returns a new vector that is the interpolation between self and goal by factor alpha (0-1)

  - vector3.new([obj]) - constructor. obj; can be used to specify initial values.

  - vector3 operators + - * ==

  - Example:

        										local vecA = vector3.new({x = 60, y = 30})
        										local vecB = vector3.new()

        										game.display_message(tostring(vecA:len())) --67.08...

        										if vecA == vecB then
        											--will not happen
        										end

        										vecA = vecA * vecB --{60*0, 30*0, 0*0}
        										if vecA == vecB then
        											--will happen
        										end

- printTable(t, [prefix]) - print an entire table, recursively. Useful for debugging.
- make(table, ...) - useful for creating table tree structures.

      								t = {}
      								  ...
      								make(t, "a", "b").c = 5
      								--t: {a = {b = {c = 5}}}

  It will make sure that table.a.b exists and return it, but won't overwrite anything that already exists.
- starts_with(str, start)
- ends_with(str, ending)
- round(num, numDecimalPlaces)
- getTime() get time in milliseconds since program start, using std::chrono::steady_clock
- _print(str) - displays str in game (console on dedicated). Regular print() uses this.
- _log(str) - adds str to rgl_log.txt
- mkdir(path) Create a directory. Returns true on success, on failure it returns nil, err_msg, err_code
  err_code can be: ERROR_ALREADY_EXISTS = 0xB7, ERROR_PATH_NOT_FOUND = 0x3
- rmdir(path) Remove a directory. Returns true on success, on failure it returns nil, err_msg, err_code
  err_code: not sure, check this page.
- lsdir(path) For listing the contents of a directory. Returns an iterator or nil, err_msg
  Iterator will return file_name, file_attributes

      								for path, attr in lsdir("dirtest") do
      									print(path, attr)
      								end

      								--Might be good to check for errors first
      								local iter, err_msg = lsdir("")
      								if iter then
      									for path, attr in iter do
      										print(path, attr)
      									end
      								else
      									print(err_msg)
      								end

- Lua startup code (sourceforge) - this gets executed right before main.lua.

Additional Libraries

- luaLanes v3.17.1 is included for multithreading. Check out Oliveran's AI! (->Examples). lanes = require("lanes").configure()
  Some things I learned while writing a leaderboard for a dedicated server:
  - Triggers don't work in a thread.
  - In general, access to the game from a thread is risky. You may get away with a lot, but the proper way is to use a worker type pattern.
    Create a worker thread that listens for a job message via a shared linda object, then do computing and send the result back to the main thread, where it is applied to the game.
  - I tried to include the sqlite library in an upvalue, it did not work. In the end I wrapped everything inside a function.
  - Runtime errors in your thread are silent. Fortunately you can wrap your worker function in a pcall and print any error.
  Full code can be found here.
- LSQLite3 v0.9.6 (sqlite 3.24.0) library for your database needs. sqlite3 = require("lsqlite3").
- lua-std-regex v1.0 (binding for C++ regex which is better than lua's) regex = require("regex").
- mobDebug, see 'Debugger' chapter.

Debugger, Profiler

#### What is a debugger?

A debugger lets you find bugs. You can step through your code line by line, set breakpoints (execution will pause), inspect variables - watch line by line how things go wrong.
Without it, you have to add print statements everywhere - it works, but for complicated bugs annoying and time consuming. For "nonlinear" (e.g. recursive) code, it can also be very hard to reconstruct the execution path.
The Lua debugger lets you debug lua - You can't see modsys or C++ code.

#### Install

1. Get ZeroBrane IDE
2. Start it, select Project->Project Directory->Choose and select your lua folder
3. Also enable Project->Start Debugger Server
4. ZeroBrane is now waiting for an incoming connection. Run local mob = require("mobDebug") in your script when you want to start debugging.
5. There should be a message: *** LUA DEBUGGER LOADED ***
6. Now: mob.start(). The game will pause and try to connect to ZeroBrane. You can also pass an address and port to start().
7. If successful, you should see a message in ZeroBrane. The game is initially paused to let you set breakpoints. Click the green arrow to continue.
8. Tip: Enable Watch/Stack window (View)

That's it! It seems like it won't detect when the game ends, so stop the debugger manually. There is also a VSCode extension, but I did not test it. Lastly, debugging will slow down lua a little bit, so don't leave it enabled when you ship your code.

More info: ZeroBrane Documentation mobDebug Github

#### Profiler

I didn't do much more than confirm that it is functional. require("jit.p") produced an error for me, but require("jit.profile").start("f", print) gave some output. (Don't stop it, it will collect data and print periodically.)
Official Documentation

Sandbox

Changes include:

- Disabled package.loadlib, package.cpath, io.popen. os.execute, os.getenv, os.tmpname, ffi library, loading bytecode (can be exploited)

- Restrict dofile, loadfile and all IO operations to the lua directory

  Access storage folder by prefixing %storage%

      										local f, err = io.open("%storage%\\file.json", "r")

  For dedicated servers, storage folder is at [warband_dir]\WSE\[Module]\ by default.
  For client, look in C:\Users\[user]\Documents\Mount&Blade Warband\WSE\[Module] (WSE 2 uses Mount&Blade Warband WSE2).
  Tip: access other modules by using %storage%\\..\\[Other Module]\\somefile.txt (this is only allowed for %storage%).

Settings (wse_settings.ini, add [lua] section)

- disable_game_const_warnings - disables all warnings during msfiles\ game constants scanning

Notice

- The Lua/C stack (used to pass parameters and return values from modsys to lua and back) is automatically cleared when it reaches a size of 100. This is to prevent stack overflows.
- When using (lua_call) operation, there is always a traceback function pushed to the stack before the call (this is to provide debug info in case of error). WSE should remove this when it's done, but it doesn't, and changing the behaviour now might break user code that relies on fixed stack indices. Therefore you have to keep this in mind when using lua_call, your stack will grow even if there are no return values. It's a good habit to use (lua_set_top, 0) before starting to call lua functions, so you always know you're starting with a clean stack.
- All lua standard libraries are loaded on game start, including bitop.
- If you don't want others to copy your source code, you can minify or obfuscate it. There are many such tools available online. Compiling modsys code is about as secure as minifying it.

##
EXAMPLES

Some of these are ripped out of larger files without testing, so be careful.

Simple custom log

Needs header_triggers.py in msfiles.

    					myLog = io.open("custom_log.txt", "a")
    					myLog:write("Server started at " .. os.date("%Y.%m.%d, %X") .. "\\n")
    					myLog:flush()

    					function getTriggerParam(index)
    							return game.store_trigger_param(0, index)
    					end

    					function playerJoinedCallback()
    							local playerNo = getTriggerParam(1)

    							game.str_store_player_username(0, playerNo)
    							game.str_store_player_ip(1, playerNo)

    							myLog:write(game.sreg[0] .. " joined with IP " .. game.sreg[1] ..
    										" at " .. os.date("%Y.%m.%d, %X") .. "\\n")
    							myLog:flush()
    							return false
    					end

    					templates = {
    							"dm",
    							"tdm",
    							"cf", --capture the flag
    							"sg", --siege
    							"bt", --battle
    							"fd", --fight and destroy
    							"ccoop", --invasion
    							"duel"
    					}

    					--add triggers to all multiplayer templates
    					for k,v in pairs(templates) do
    						game.addTrigger("mst_multiplayer_" .. v, game.const.ti_server_player_joined, 0, 0, playerJoinedCallback)
    					end

Squad spawner

This function spawns agents in a square at the specified position.

    					function spawnSquad(troop, amount, position)
    							local sideLen = math.floor(math.sqrt(amount))
    							local leftovers = amount - sideLen * sideLen
    							local spawnSpreadDistance = 1
    							position.o.x = position.o.x - math.floor(sideLen/2) * spawnSpreadDistance
    							position.o.y = position.o.y - math.floor(sideLen/2) * spawnSpreadDistance

    							for i=1, sideLen do
    									for j=1, sideLen do
    											game.set_spawn_position(position)
    											game.spawn_agent(troop)
    											position.o.x = position.o.x + spawnSpreadDistance
    									end
    									position.o.x = position.o.x - spawnSpreadDistance * sideLen
    									position.o.y = position.o.y + spawnSpreadDistance
    							end
    					end

You could call it like:

    					local pos = game.pos.new({o = {x=100,y=100}})
    					spawnSquad(game.const.trp_bandit, 10, pos)

Quickly reload main.lua when typing "reloadMain" in server console

All this does is call dofile. Be careful, it is not equivalent to a full restart.

    					("wse_console_command_received", [ #this is a script that WSE adds
    							(store_script_param, ":command_type", 1),

    							(str_equals, s0, "@reloadMain"),
    							(lua_push_str, "@main.lua"),
    							(lua_call, "@dofile", 1), #one param, the string we pushed
    							(set_trigger_result, 1),
    					]),

event_mgr.lua

The idea is to have a wrapper for game.addTrigger so we have more control (for example this is useful for hot reloading), and to have a single interface for all events - mission template, scene prop, and item triggers, as well as your own events.
It returns a constant index to your trigger. game.addTrigger index will shift if you remove things below.
Does only add to deathmatch, and no management of SP/MP triggers. It's up to you to modify to your needs.
Needs header_triggers.py, ID_scenes.py, ID_items.py in msfiles.

    					--[[
    					Usage:
    						event_mgr.subscribe(event_id, callback)
    						event_id:
    							"ti_constant", e.g. "ti_before_mission_start"
    							"timer_x.y", e.g. "timer_1.5" or "timer_0"
    							"itm_a:ti_constant", e.g. "itm_french_cav_pistol:ti_on_weapon_attack"
    							"spr_b:ti_constant", same thing
    							"script_", e.g. "script_game_quick_start" (versus hookScript - you can not control execution of modsys script here)
    							"key_", e.g.:
    							"key_o"                              O clicked
    							"key_o down=key_shift"               O clicked while Shift down
    							"key_k down=key_shift key_control"   key_shift, key_control means both left/right
    							"your_own_event_id", can be used with dispatch()

    						returns: a subscription ID which you can use with unsubscribe

    						event_mgr.unsubscribe(event_id, subscription_id)

    						event_mgr.clear()
    						Clear all callbacks. Useful for hot-reloading
    						Example reload:
    						event_mgr.subscribe("key_r down=key_shift", function()
    							event_mgr.clear()
    							print("Reloading")
    							dofile("main.lua")
    						end)

    						event_mgr.dispatch(event_id, ...)
    					]]

    					local regex = require "regex"

    					if not event_mgr then
    						event_mgr = {
    							events = {}
    						}
    					end

    					function event_mgr.subscribe(event_id, callback)
    						event_mgr.init_event(event_id)

    						local i = 1
    						while event_mgr.events[event_id][i] ~= nil do i = i + 1 end
    						event_mgr.events[event_id][i] = callback
    						return i
    					end

    					function event_mgr.unsubscribe(event_id, subscription_id)
    						event_mgr.events[event_id][subscription_id] = nil
    					end

    					function event_mgr.dispatch(event_id, ...)
    						if event_mgr.events[event_id] then
    							for _, event_callback in pairs(event_mgr.events[event_id]) do
    								event_callback(...)
    							end
    						end
    					end

    					function event_mgr.clear()
    						for event_id, _ in pairs(event_mgr.events) do
    							event_mgr.events[event_id] = {}
    						end
    					end

    					function event_mgr.init_event(event_id)
    						if event_mgr.events[event_id] then return end
    						make(event_mgr.events, event_id)

    						local function add_mst_trig(const, callback)
    							for i = 0, game.getNumTemplates()-1 do
    								game.addTrigger(i, const, 0, 0, callback)
    							end
    						end

    						--generic dispatcher
    						local function cb()
    							event_mgr.dispatch(event_id)
    							return false
    						end

    						if starts_with(event_id, "ti_") then
    							local const = game.const.triggers[event_id]
    							add_mst_trig(const, cb)

    						elseif starts_with(event_id, "timer_") then
    							local const = tonumber(string.match(event_id, "%d+%.?%d*"))
    							add_mst_trig(const, cb)

    						elseif starts_with(event_id, "itm_") then
    							local itm, const = string.match(event_id, "([%w_]+):([%w_]+)")
    							itm = game.const[itm]
    							const = game.const[const]
    							game.addItemTrigger(itm, const, cb)

    						elseif starts_with(event_id, "spr_") then
    							local spr, const = string.match(event_id, "([%w_]+):([%w_]+)")
    							spr = game.const[spr]
    							const = game.const[const]
    							game.addScenePropTrigger(spr, const, cb)

    						elseif starts_with(event_id, "script_") then
    							local s = string.match(event_id, "script_([%w_]+)")
    							game.hookScript(game.script[s], function(...) event_mgr.dispatch(event_id, ...) end)

    						elseif starts_with(event_id, "key_") then
    							local keyname, modkeys = regex.match(event_id, [[(key_\w+)(?: down=(.+))?]])
    							local down = {}

    							local function key_test_func(keyname, op)
    								if keyname == "key_control" then
    									local k1 = game.const.triggers["key_left_control"]
    									local k2 = game.const.triggers["key_right_control"]
    									return function() return (op(k1) or op(k2)) end

    								elseif keyname == "key_shift" then
    									local k1 = game.const.triggers["key_left_shift"]
    									local k2 = game.const.triggers["key_right_shift"]
    									return function() return (op(k1) or op(k2)) end

    								elseif keyname == "key_alt" then
    									local k1 = game.const.triggers["key_left_alt"]
    									local k2 = game.const.triggers["key_right_alt"]
    									return function() return (op(k1) or op(k2)) end

    								else
    									local k = game.const.triggers[keyname]
    									return function() return op(k) end
    								end
    							end

    							local clicked = key_test_func(keyname, game.key_clicked)
    							for modkey in regex.gmatch(modkeys, [[\w+]]) do
    								table.insert(down, key_test_func(modkey, game.key_is_down))
    							end

    							add_mst_trig(0, function()
    								if clicked() then
    									for i = 1, #down do
    										if not down[i]() then return false end
    									end

    									event_mgr.dispatch(event_id)
    									return false
    								end
    							end)
    						end
    					end

    					--For calling from modsys
    					function eventDispatch(event_id, ...)
    						event_mgr.dispatch(event_id, ...)
    					end

Timeouts

Execute modssys script or lua function with a delay. Uses previous event manager.

    					local timeouts = {}

    					function timeoutsTick()
    						if #timeouts ~= 0 then
    							local t_now = game.store_mission_timer_a_msec(0)

    							for i = #timeouts, 1, -1 do
    								if timeouts[i].t <= t_now then
    									timeouts[i].cb(timeouts[i].cb, t_now)
    									table.remove(timeouts, i)
    								end
    							end
    						end
    					end

    					function timeoutAddScript(time, script_no, ...)
    						local args = {...}
    						local cb = function()
    							game.call_script(script_no, unpack(args))
    						end

    						table.insert(timeouts, { t = game.store_mission_timer_a_msec(0) + time, cb = cb})
    					end

    					function timeoutAdd(time, callback)
    						table.insert(timeouts, { t = game.store_mission_timer_a_msec(0) + time, cb = callback })
    					end

    					event_mgr.subscribe("ti_before_mission_start", function() timeouts = {} end)
    					event_mgr.subscribe("timer_0", timeoutsTick)

Modsys example:

    					(lua_push_int, 2000),
    					(lua_push_int, "script_for_timeout"),
    					(lua_push_int, ":var1"),
    					(lua_push_int, ":var2"),

    					(lua_call, "@timeoutAddScript", 4),
    					(lua_set_top, 0), #Just make sure its cleaned up

Map Scripts

Before map start, searches for ./lua/map_scripts/<mapname>.lua and executes it in its own environment (using setfenv).
Uses previous event manager. The script can read from root _G via the metatable, event_mgr is wrapped though in order to auto-unsub any events at map change.
Needs ID_scenes.py in msfiles.

    					local subbed_events = {}

    					--wrap event_mgr so we can log all subscriptions and auto-unsub at map change
    					local fenv_mt = {
    					  __index = setmetatable({
    					    event_mgr = {
    					      subscribe = function(event_id, callback)
    					        local idx = _G.event_mgr.subscribe(event_id, callback)
    					        table.insert(subbed_events, {event_id = event_id, idx = idx})
    					        return idx
    					      end,

    					      unsubscribe = function(event_id, index)
    					        for i,v in ipairs(subbed_events) do
    					          if v.idx == index then
    					            table.remove(subbed_events, i)
    					            return _G.event_mgr.unsubscribe(event_id, index)
    					          end
    					        end
    					      end,

    					      dispatch = _G.event_mgr.dispatch
    					    }
    					  }, {__index = _G})
    					}

    					local scene_names = {}
    					for k,v in pairs(game.const.scenes) do scene_names[v] = k end

    					local function load_map_script(map_name)
    					  local fname = "map_scripts\\" .. map_name .. ".lua"
    					  local f = io.open(fname, r)
    					  if not f then return end
    					  f:close()

    					  local func, error = loadfile(fname)
    					  if not func then print("Error loading " .. fname .. ": " .. error); return end

    					  local fenv = {}
    					  setmetatable(fenv, fenv_mt)

    					  setfenv(func, fenv)
    					  func()
    					end

    					event_mgr.subscribe("ti_before_mission_start", function()
    					  --clean events from last map
    					  for _,v in pairs(subbed_events) do
    					    event_mgr.unsubscribe(v.event_id, v.idx)
    					  end
    					  subbed_events = {}

    					  local scene = game.store_current_scene(0)
    					  load_map_script(scene_names[scene])
    					end)

Example: lua/map_scripts/scn_mp_custom_map_3.lua

    					print("custom_map_3 script loaded")

    					event_mgr.subscribe("spr_apple_a:ti_on_init_scene_prop",
    					  function()
    					    print("apple")
    					    do_something()
    					  end)

    					event_mgr.subscribe("itm_birch_trunk:ti_on_weapon_attack",
    					  function()
    					    print("attack")
    					    explode_player()
    					  end)

Presentation with a clock and a white center dot (for aiming)

Needs header_triggers.py, header_presentations.py, ID_meshes.py in msfiles.

    					local clockOverlay
    					local nextRefreshTime

    					local prsnt = game.addPrsnt({
    						id = "clock",
    						flags = {game.const.prsntf_read_only, game.const.prsntf_manual_end_only},
    						triggers = {
    							[game.const.ti_on_presentation_load] = function()
    								game.presentation_set_duration(99999999)

    								clockOverlay = game.create_text_overlay(0, "00:00:00", game.const.tf_left_align)

    								local screenPos = game.pos.new()
    								screenPos.o.x = 0
    								screenPos.o.y = 0

    								local sizePos = game.pos.new(game.preg[0])
    								sizePos.o.x = 1
    								sizePos.o.y = 1

    								game.overlay_set_position(clockOverlay, screenPos)
    								game.overlay_set_size(clockOverlay, sizePos)
    								game.overlay_set_color(clockOverlay, 0xFFFFFA)

    								nextRefreshTime = 0

    								midDotOverlay = game.create_mesh_overlay(0, game.const.meshes.mesh_white_dot)
    								screenPos.o.x = 0.5 - 0.0002
    								screenPos.o.y = 0.75/2 - 0.0001
    								sizePos.o.x = 0.1
    								sizePos.o.y = 0.1
    								game.overlay_set_position(midDotOverlay, screenPos)
    								game.overlay_set_size(midDotOverlay, sizePos)
    							end,

    							[game.const.ti_on_presentation_run] = function()
    								local curTime = game.store_trigger_param_1(0)
    								if curTime >= nextRefreshTime then
    									nextRefreshTime = nextRefreshTime + 1000
    									game.overlay_set_text(clockOverlay, os.date("%X"))
    								end
    							end
    						}
    					})

    					event_mgr.subscribe("ti_after_mission_start", function()
    						game.start_presentation(prsnt)
    					end)

At map start, spawn banhammer at each apple_a with varno1 = 1

Uses event_mgr, needs header_triggers.py, ID_items.py in msfiles.

    					event_mgr.subscribe("spr_apple_a:ti_on_init_scene_prop",
    					  function()
    					    local inst = game.store_trigger_param_1(0)
    					    if game.prop_instance_get_variation_id(0, inst) == 1 then
    					    	print("found apple " .. inst)

    					    	game.prop_instance_get_position(1, inst) --pos1 translates to 1
    					    	game.set_spawn_position(1)
    					    	game.spawn_item(game.const.itm_banhammer, 0, 0)
    					    end
    					  end)

Oliveran's Combat AI (multithreaded)

This was made by Oliveran in 2017. I never ran this myself or reviewed the code in any detail, but the results speak for themselves.

Original description:

I'm a long-time competitive player, who's always really enjoyed all aspects of singleplayer - but as I increased in skill, the AI got dull very quickly, as I grew with the competitive scene's skill. As the game has reached a relatively stagnant point in terms of figuring out mechanics, break down playstyles and ways to play as a team; I figured I could use the knowledge to make an AI that can play more player-like, that can be functional in both singleplayer and mutiplayer (though ofc not at a competitive level )

Videos (Youtube):

Code
Forum Post
Code extract - Dedicated Server

Some of the lua code I made for a server. Including a stats database and animation helper.

Github