version from 26.04.2017, written by AgentSmith

How to use WSE/LUA

 

 

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

There is some example code at the end of this file that can help you to get started.

 

 
BASIC SETUP


 

 
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

intResult, intError

boolDidNotFail, intResult, intError

intError

=  game.cf_operation(arg1, arg2, ...) for can_fail operations

=  game.lhs_operation(lhs_arg, arg1, arg2, ...) for lhs operations

=  game.cf_lhs_operation(lhs_arg, arg1, arg2, ...) for operations which are both can_fail and lhs

=  game.operation(arg1, arg2, ...) for all other operations

While most Lhs operations don't require an actual value for lhs_arg (in those cases it's strictly a return variable, for example player_get_gold), some (for example val_add) do, so you have to specify it anyway.

Real-world lhs_operation example:

local gold = game.player_get_gold(0, playerId)

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.

Registers

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

Example:

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

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]\n p:rotX(30)\n game.preg[0] = p\n
Constants

For every header_[name].py or ID_[name].py file you copied to your "msfiles" folder (except header_operations.py), a new table will be generated: game.const.[name]

This table will be filled with all the constants from that file.

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+)?))$\n ^(\w+)=(\w+)$\n

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 and a warning message will be triggered which you can safely ignore.

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)\n

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)\n --or if you like to be specific:\n game.addTrigger("mst_multiplayer_dm", game.const.triggers.ti_on_agent_hit, 0, 0, condCB)\n
Triggers

To add triggers, use game.addTrigger(strTemplateId, 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. The callbacks must return a boolean. If conditionsCallback returns false, consequencesCallback will not be executed.

This function is fairly expensive, as it involves copying the entire array of triggers and then adding the new one.

Example:

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

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

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

Iterators

To use iterators (try_for_agents, try_for_players, ...), use the provided iterator functions game.partiesI, game.agentsI, game.propInstI, game.playersI

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

Example:

for curAgent in game.playersI(true) do --skip server\n foo(curAgent)\n end\n
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.

MS Example:

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

Consider the lua counterpart to "always have a fixed point multiplier of 1".

  • 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\n f = vector3.new({y = 1}) --forwards/y axis\n u = vector3.new({z = 1}) --up/z axis\n function getRot(self) --returns vector3.new({z = yaw, x = pitch, y = roll})\n function rotX(self, angle) --rotate around x axis, angle in degrees\n function rotY(self, angle) --rotate around y axis, angle in degrees\n function rotZ(self, angle) --rotate around z axis, angle in degrees\n function rotate(self, rotVec3) --rotate around all axis, zxy order, angle in degrees\n
  • 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\n rot = game.rotation.new() --rotation, or heading of the pos if you like\n \n --see game.rotation.prototype for these\n function getRot(self)\n function rotX(self, angle)\n function rotY(self, angle)\n function rotZ(self, angle)\n function rotate(self, rotVec3)\n

Example:

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

You can add presentations using game.addPrsnt(tablePrsnt)

tablePrsnt must have the following format

tablePrsnt = {\n \tabid = strID,\n \tab[flags] = {intFlag1, ...},\n \tab[mesh] = intMesh,\n \tabtriggers = {[numTriggerConst] = func, ...}\n

}

[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.

Basic example:

--copy header_presentations.py, header_triggers.py and ID_meshes.py to your msfiles folder for this example.\n local index = game.addPrsnt({\n \tabid = "myPrsnt",\n \tabflags = {game.const.prsntf_read_only, game.const.prsntf_manual_end_only},\n --you can initialize an array like this, without keys - they don't matter here anyway.\n \tabmesh = game.const.mesh_cb_ui_main,\n \tabtriggers = {\n \tab2[game.const.ti_on_presentation_load] = function ()\n --The const inside the [] declares a number key, similar to keyName = 123 for string keys.\n \tab3game.presentation_set_duration(9999999)\n \tab3local overlay = game.create_mesh_overlay(0, game.const.mesh_mp_ingame_menu)\n \n \tab3local position = game.pos.new()\n \tab3position.o.x = 0.3\n \tab3position.o.y = 0.3\n \tab3game.overlay_set_position(overlay, position)\n \n \tab3position.o.x = 0.5\n \tab3position.o.y = 0.8\n \tab3game.overlay_set_size(overlay, position)\n \tab2end\n \tab}\n })\n game.start_presentation(index)\n
Other functions:
  • game.getScriptNo(script_name), use in conjunction with game.call_script(scriptNo, ...)

    Example:

    local no = game.getScriptNo("game_start")\n game.call_script(no)\n
  • 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.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.\n 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().\n Any error (which would normally trigger the event again, which would trigger the error, ...) caused by this function will be catched and logged safely.

 

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.getReg(intTypeId, intIndex) typeIds are: 0 = int, 1 = str, 2 = pos

  • game.setReg(intTypeId, intIndex, val)

 
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\n y = 0\n z = 0\n function len(self) --returns the length of the vector\n
    • vector3.new([obj]) - constructor. obj; can be used to specify initial values.

    • vector3 operators + - * ==

    • Example:

      local vecA = vector3.new({x = 60, y = 30})\n local vecB = vector3.new()\n \n game.display_message(tostring(vecA:len())) --67.08...\n \n if vecA == vecB then\n \tab--will not happen\n end\n \n vecA = vecA * vecB --{60*0, 30*0, 0*0}\n if vecA == vecB then\n \tab--will happen\n end\n
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

 
EXAMPLES


Simple custom log
myLog = io.open("custom_log.txt", "a")\n myLog:write("Server started at " .. os.date("%Y.%m.%d, %X") .. "\\n")\n myLog:flush()\n \n function getTriggerParam(index)\n \tabreturn game.store_trigger_param(0, index)\n end\n \n function playerJoinedCallback()\n \tablocal playerNo = getTriggerParam(1)\n \n \tabgame.str_store_player_username(0, playerNo)\n \tabgame.str_store_player_ip(1, playerNo)\n \n \tabmyLog:write(game.sreg[0] .. " joined with IP " .. game.sreg[1] ..\n \tab2" at " .. os.date("%Y.%m.%d, %X") .. "\\n")\n \tabmyLog:flush()\n \tabreturn false\n end\n \n templates = {\n \tab"dm",\n \tab"tdm",\n \tab"cf", --capture the flag\n \tab"sg", --siege\n \tab"bt", --battle\n \tab"fd", --fight and destroy\n \tab"ccoop", --invasion\n \tab"duel"\n }\n \n --add triggers to all multiplayer templates\n for k,v in pairs(templates) do\n \tabgame.addTrigger("mst_multiplayer_" .. v, game.const.ti_server_player_joined, 0, 0, playerJoinedCallback)\n end\n
Squad spawner

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

function spawnSquad(troop, amount, position)\n \tablocal sideLen = math.floor(math.sqrt(amount))\n \tablocal leftovers = amount - sideLen * sideLen\n \tabposition.o.x = position.o.x - math.floor(sideLen/2) * spawnSpreadDistance\n \tabposition.o.y = position.o.y - math.floor(sideLen/2) * spawnSpreadDistance\n \n \tabfor i=1, sideLen do\n \tab2for j=1, sideLen do\n \tab3game.set_spawn_position(position)\n \tab3game.spawn_agent(troop)\n \tab3position.o.x = position.o.x + spawnSpreadDistance\n \tab2end\n \tab2position.o.x = position.o.x - spawnSpreadDistance * sideLen\n \tab2position.o.y = position.o.y + spawnSpreadDistance\n \tabend\n end\n

You could call it like:

local pos = game.pos.new({o = {x=100,y=100}})\n spawnSquad(game.const.trp_bandit, 10, pos)\n
Quickly reload main.lua when typing "reloadMain" in server console
("wse_console_command_received", [ --this is a script that WSE adds\n \tab(store_script_param, ":command_type", 1),\n \n \tab(str_equals, s0, "@reloadMain"),\n \tab(lua_push_str, "@main.lua"),\n \tab(lua_call, "@dofile", 1),\n \tab(set_trigger_result, 1),\n ]),\n