【翻译】如何在WSE使用lua进行MOD开发

llqs 发表于 2025-06-14 20:15

浏览 35

回复 0

写在前面:
新三年,旧三年,缝缝补补又三,说的就是战团mod和开发者们。
如今有了WSE2的助力,可以说是“枯木逢春”亦不为过。
WSE2为开发者进行了提供了许多扩展能力,前面@zz010606 已经连续发布多个帖子,相信会给大家不少启发。
WSE2的另一个重要能力,就是集成了AgentSmith开发的lua支持。使得大家在module system之外,又多了一种开发mod的方式。
WSE2优先执行lua代码,也就是不管mod有没有代码,你都可以直接使用lua进行功能开发。
且lua是在战团引擎之外执行,就这一点而言,它应该极有可能改善战团mod的性能。
所以,我翻译了WSE2 开发包中自带的lua开发指导,发帖于此,方便大家查阅。
众人拾柴火焰高,希望战团,尤其是国内战团,真的能迎来“又一春”!


附上:
WSE2 K700的原贴
WSE2 网盘 1.1.3.6分流下载(原始发布地址需要梯子)
技术交流群: 68181940  (骑砍技术小黑屋)


======================== 以下为译文
版本日期为2025年4月6日,由forums.taleworlds.com的AgentSmith编写

在这个文件末尾有一些示例代码,可以帮助你上手。

[spoiler=基础内容]

为什么使用Lua?
语法更优美,数据结构(数组不再有固定的存储槽位),具备返回值的真正函数,简单的字符串处理等等。
无需编译,无需Python
(无需重启即可测试更改)
提供调试器(可让你逐行浏览代码)
更好的性能
多线程(luaLanes库)
使用互联网上的任何Lua代码
没有令人困惑的定点乘法器,简单的(向量)数学运算

而不是
 

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


设置
像往常一样安装WSE,确保lua51.dll与mb_warband.exe在同一文件夹中
创建[战团目录]\Modules\[模组名称]\lua\,例如 C:\Program Files (x86)\Steam\steamapps\common\MountBlade Warband\Modules\Native\lua\
在Lua中也是一样的,但你要在运行时在main.lua中注册触发器(或者加载一个执行此操作的文件)。
创建[战团目录]\Modules\[模组名称]\lua\msfiles\ (msfiles - “模组系统文件”)
将header_operations.py复制到你的msfiles文件夹中。这样WSE就可以将操作名称转换为操作码,这对于从lua调用模块系统操作是必要的。

现在开始游戏,看看解析这个文件是否有错误,这是个好主意。删除任何错误行,但要确保所需的任何操作码以及末尾的列表(如lhs_operations = [...]等)保持完整。
WSE/LUA 使用的是 LuaJIT 2.1.0 的修改版本,该版本基于 Lua 5.1 并与其应用二进制接口(ABI)兼容。

用于测试代码片段和调试的Lua集成开发环境

[/spoiler]

[spoiler="game"表]
游戏表是一个预定义的全局表,允许你从Lua访问游戏。

操作

要调用操作,请使用game.operation_name(arg1, arg2, ...)

示例:game.display_message("测试字符串")

字符串和位置参数通过游戏寄存器传递给游戏(覆盖寄存器中的值),从reg[128]开始并向下计数。

请注意,参数类型很重要。例如,如果一个操作需要一个整数,你就不能传入一个包含数字的字符串。

返回值为:
 

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)对于可能失败的操作:
`boolDidNotFail`(操作是否未失败), `intError`(错误码) = `game.cf_operation(arg1, arg2, ...)`

对于左值操作:
`intResult`(结果值), `intError`(错误码) = `game.lhs_operation(lhs_arg, arg1, arg2, ...)`

对于既是可能失败又是左值的操作:
`boolDidNotFail`(操作是否未失败), `intResult`(结果值), `intError`(错误码) = `game.cf_lhs_operation(lhs_arg, arg1, arg2, ...)`

对于所有其他操作:
`intError`(错误码) = `game.operation(arg1, arg2, ...)`

(lhs = 左值?指赋值操作,例如 `val_add`)
(cf = 可能失败,例如 `agent_is_active`)


请注意,在Lua中,你不必赋值所有的返回值。例如,如果你不关心错误代码,只需省略相应的左值变量。

我不确定错误代码表示什么,不过如果没有错误,它应该是0。


对于左值(Lhs)操作,存在一个小问题。其语法为:

(lhs_op,<目标_参数>,<参数1>,<参数2>,…)

而在Lua中,你需要这样写:
result = game.lhs_op(param1, param2, ...)

一个明显的例子是`val_add`。写成`result = val_add(5)`是没有意义的。它要把5加到什么上面去呢?

是否一个操作使用初始值无法简单地通过WSE检测出来。所以过去,对于所有的左值操作,你都得写成result = game.lhs_op(some_number, var1, var2, ...)(其中some_number大多为0)。这很烦人。

然而,经过进一步检查,可以合理假设只有val_操作会这样。因此,WSE更新添加了game.op:
 

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


对于任何以lhs_开头的操作,它将自动添加0,但以val_开头的操作除外。如果你发现此规则不适用的操作,请在TaleWorlds论坛上联系特工史密斯。

gop = game.op 这种写法也可行。


寄存器,全局变量

要访问寄存器,请使用game.reg[n]、game.sreg[n]和game.preg[n]

示例:
 

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]


截至当前版本,寄存器是按值复制而非按引用复制。因此,像这样操作:
 

game.preg[0]:rotX(30)


不会更改实际的游戏寄存器。这一点可能会在未来版本中得到改进。目前你需要做的是:
 

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


要访问全局变量,请使用game.gvar.gvar_name
 

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


WSE将在启动时读取variables.txt文件来翻译全局变量名。如果由于任何原因该文件不正确,访问全局变量可能会产生错误或访问到随机的全局变量。你无法创建新的全局变量。
        

常量

如果有两个文件名相同,例如header_scenes和ID_scenes,这些表格将被合并。

截至当前版本,只有与以下两个正则表达式之一匹配的行才会被接受:
 

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


这可能看起来令你望而生畏,但实际上它只是说你的常量必须是一个数字、一个十六进制数,或者(当前来自同一文件中的)另一个常量。

换句话说:常量 = 数字|十六进制数字|其他常量

如果匹配失败,该行将被忽略。若要在这种情况下显示警告,请在wse_settings.ini中设置[lua] disable_game_const_warnings = 0。

你可以省略`game.const.[name].[constant]`中的`[name]`部分,在这种情况下,将在`game.const`中的所有表中搜索你的常量,并使用第一个结果。

所以假设你想为ti_on_agent_hit添加一个死斗触发条件。不要这样写:
 

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


这可能看起来令你望而生畏,但实际上它只是说你的常量必须是一个数字、一个十六进制数,或者(当前来自同一文件中的)另一个常量。

换句话说:常量 = 数字|十六进制数字|其他常量

如果匹配失败,该行将被忽略。若要在这种情况下显示警告,请在wse_settings.ini中设置[lua] disable_game_const_warnings = 0。

你可以省略`game.const.[name].[constant]`中的`[name]`部分,在这种情况下,将在`game.const`中的所有表中搜索你的常量,并使用第一个结果。

所以假设你想为ti_on_agent_hit添加一个死斗触发条件。不要这样写:
 

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


你可以将header_triggers.py复制到你的msfiles文件夹中,然后执行:
 

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)



触发器
要添加任务模板触发器,请使用 `index = game.addTrigger(strTemplateId|intTemplateNo, numCheckTime, numDelayTime, numRearmTime, funcConditionsCallback, [funcConsequencesCallback])`。

这些的工作方式与模块系统触发器完全相同。时间可以是浮点值,也可以是header_triggers.py中的值(数字,目前还不支持字符串名称),就像在模块系统中一样。如果conditionsCallback返回false,则不会执行consequencesCallback。如果它返回true或不返回任何值,则会执行consequencesCallback。

示例:
 

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



addTrigger返回一个整数,该整数是添加的触发器的索引。

你也可以使用game.removeTrigger(strTemplateId|intTemplateNo, intTriggerIndex)移除触发器。如果intTriggerIndex为负数,则会移除(numTriggers + intTriggerIndex)对应的触发器(例如,要移除最后一个触发器,使用 -1)。返回是否成功。

有用的函数:

`game.getCurTemplateNo()`函数返回当前任务模板的索引。请注意,在执行`main.lua`时,不会加载任何模板。
`game.getCurTemplateId()`返回当前任务模板的ID/名称。请注意,在执行`main.lua`时不会加载任何模板。
game.getNumTemplates()返回任务模板的数量。
game.getTemplateId(index)返回位于索引位置的任务模板的ID/名称。
game.getNumTriggers(strTemplateId|intTemplateNo) 返回此任务模板的触发器数量。

物品触发器:
index   = game.addItemTrigger(strItemID|intItemNo, numTriggerInterval, funcCallback) 可翻译为:index = game.addItemTrigger(物品字符串ID|物品编号, 触发间隔数值, 回调函数)
success = game.removeItemTrigger(strItemID|intItemNo, index) 翻译为:success = game.removeItemTrigger(字符串物品ID|整数物品编号, 索引)
num=game. getNumItem触发器(strItemID|intItemNo)
示例:
 

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)



场景道具触发器:
index   = game.addScenePropTrigger(场景道具ID|场景道具编号, 触发间隔时间, 回调函数)
success = game.removeScenePropTrigger(strPropID|intPropNo, index)  # 成功 = game.removeScenePropTrigger(字符串道具ID|整数道具编号, 索引)
num     = game.getNumScenePropTriggers(strPropID|intPropNo)
示例:
 

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)



世界触发器:

由于技术原因,这些功能的工作方式有所不同。你需要为game.OnWorldTrigger指定一个回调函数:
 

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


根据WSE源代码进行的时间换算:

小时 = 日期
天数 = 日期 / 24
周数 = 日期 / 168
月份 = 日期 / 720
年  = 日期 / 8640
由于技术原因,如果在Lua中添加的触发器存在modsys错误(例如无效的智能体ID),它将报告为第0行,操作码5113。

迭代器
要使用迭代器(try_for_agents、try_for_players 等),请使用提供的迭代器函数 game.partiesIt、game.agentsIt、game.propInstIt、game.playersIt。

这些的工作方式与模块系统迭代器完全相同,并接受相同的参数。
 

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])


示例:
 

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



位置
要使用位置,提供了以下 “类”:game.rotation、game.pos 和 vector3(见杂项部分)

请记住,这些不需要模块系统要求你使用的定点乘法器。

MS乘数本质上是一种指定当前长度单位的方法。
 

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


在Lua中,情况更简单,你使用浮点数。“1.0”始终表示1米,“0.01”始终表示1厘米。
 

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


游戏轮播

game.rotation.new([obj]) - 构造函数。obj 可用于指定初始值。

游戏。旋转。原型
 

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]) - 构造函数。obj 可用于指定初始值。

game. pos.原型
 

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)



示例:
 

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})



界面presentation
你可以使用 `index = game.addPrsnt(tablePrsnt)` 添加演示文稿。

tablePrsnt必须采用以下格式

 

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



["flags"] 可以省略,默认不使用任何标志。如果你将 header_presentation.py 复制到了你的 msfiles 文件夹中,你可以使用在那里定义的标志。

[网格] 可以省略,默认值为0。如果你将ID_meshes.py复制到你的msfiles文件夹中,你可以使用在那里定义的网格编号。

触发器必须至少有一个元素,键为数字,值为函数。如果你将header_triggers.py复制到你的msfiles文件夹中,你可以使用在那里定义的触发器值。

返回值:新演示文稿的索引。

示例:
 

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



使用game.removePrsnt(index)进行移除。请记住,移除操作会使上方的索引值发生移动。

在专用服务器上,添加/移除操作将执行但不返回任何内容。

由于技术原因,如果在Lua中添加的演示文稿中存在modsys错误(例如无效的智能体ID),它将报告为第0行,操作码5113。

粒子系统

你可以使用 `index = game.addPsys(tablePsys)` 添加粒子系统。

tablePsys必须采用以下格式

 

tablePsys = {
id = 字符串, [flags] = {整数, 整数, ...},
mesh = 字符串|整数,num_particles = 整数, life = 数字, damping = 数字, gravity_strength = 数字, turbulance_size = 数字, turbulance_strength = 数字,
alpha_keys                        = {{数字, 数字}, {数字, 数字}},
red_keys                        = {{数字, 数字}, {数字, 数字}},
green_keys                        = {{数字, 数字}, {数字, 数字}},
blue_keys                        = {{数字, 数字}, {数字, 数字}},
scale_keys                        = {{数字, 数字}, {数字, 数字}},
emit_box_size                = {数字, 数字, 数字},
emit_velocity                = {数字, 数字, 数字},
emit_dir_randomness = 数字,
rotation_speed = 数字,
rotation_damping = 数字}



[标志] 可以省略,默认不使用任何标志。

返回值:新粒子系统的索引。

示例:

 

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)



使用game.removePsys(index)移除。请记住,移除操作会使上方的索引值发生移动。

在专用服务器上,添加/移除操作会执行,但不会返回任何内容。

模块操作挂钩
你可以使用 `game.hookOperation(operation, funcCallback)` 来挂钩模块操作。

这意味着只要模块系统使用该操作,您的回调就会首先执行。

操作必须是作为字符串的操作名称或操作码整数。

操作钩子仍处于试验阶段 - 它可能非常有用,但有崩溃风险。



funcCallback的返回值控制后续行为。

如果它返回空值或true,模块操作将照常执行。

如果第一个返回值为假,则不会执行模块操作。

如果第二个返回值是布尔值,模块系统将认为它正在执行一个条件判断操作(因此,如果布尔值为假,它将中断执行,例如(eq, 1, 0) 。)

如果第二个返回值是一个数字,模块系统将认为它正在执行左值操作,并尝试设置返回值。

如果回调返回三个值,模块系统将认为它正在执行cf/lhs操作。第二个返回值必须是cf值,第三个返回值必须是lhs值。

简而言之,变体如下:
 

boolExecute
boolExecute, boolFail
boolExecute, numLhs
boolExecute, boolFail, numLhs boolExecuteboolExecute,boolFailboolExecute,numLhsboolExecute,boolFail,numLhs



要取消挂钩某个操作,请使用game.hookOperation(operation, nil)。

从Lua调用操作时,该钩子不会被触发。不过,这个任务可以通过覆盖游戏元表来实现,其定义如下:
 

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



例如,可以执行(尚未完成):
 

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)



你甚至可以更进一步,为game.hookOperation创建一个包装器,自动为你完成这个过程。

脚本挂钩
你可以使用game.hookScript(script_no, funcCallback)挂钩脚本。

`funcCallback` 将接收所有脚本参数。其返回值控制后续行为:

如果它返回false,则不会执行该脚本。

如果返回的是数字,脚本参数将被这些数字替换,然后执行脚本。



示例:
 

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)



要取消挂钩一个脚本,使用 `game.hookScript(script_no, nil)`。

其他函数
game.getScriptNo(script_name) 需与 game.call_script(scriptNo, ...) 结合使用

示例:
 

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



WSE更新现在提供了一个包装器:game.script.[名称]
 

game.call_script(game.script.game_start)



`game.getCurTemplateNo()`函数返回当前任务模板的索引。请注意,在执行`main.lua`时,不会加载任何模板。
`game.getCurTemplateId()`返回当前任务模板的ID/名称。请注意,在执行`main.lua`时不会加载任何模板。
game.getNumTemplates()返回任务模板的数量。
game.getTemplateId(index)返回位于索引位置的任务模板的ID/名称。
game.getNumTriggers(strTemplateId|intTemplateNo) 返回此任务模板的触发器数量。
game.getMeshId(index) 返回索引(网格编号)处网格的名称。注意:这不会访问module_meshes中的网格列表。索引针对的是从.brf文件加载的所有网格的内部列表。
`game.getNumMeshes()` 返回网格数量。注意:此操作不会访问 `module_meshes` 中的网格列表。它针对的是从 `.brf` 文件加载的所有网格的内部列表。
当有内容写入rgl_log.txt时,`game.OnRglLogWrite(str)` 这个函数(如果存在)会被调用。它接收日志消息作为参数。在这个函数中使用 `game.display_message` 不是个好主意,因为这很容易导致无限循环。不过,你可以使用 `print()`。此函数引发的任何错误(通常会再次触发该事件,进而再次引发错误……)都会被捕获并安全记录。
当接收到聊天消息时(客户端和服务器端均适用),如果存在`game.OnChatMessageReceived(intPlayerNo, boolIsTeamChat, strMsg)`这个函数,它就会被调用。该函数会在相应的模块系统脚本之后被调用。如果此函数返回一个字符串,则消息将被覆盖;如果返回`true`,则消息将被抑制。返回任何内容都将覆盖模块脚本可能采取的任何操作。

游戏加载时,如果存在`game.OnGameLoad()`这个函数,将会调用它。

游戏保存成功后,若存在此函数,将会调用 `game.OnSave()` 。

如果存在 `game.OnLoadSave()` 这个函数,那么在存档成功加载后会调用该函数。

在lua_call操作期间使用`game.fail()`会导致MS代码失败,就像任何其他cf操作都可能导致的那样。

示例:
 

(try_begin),
                (lua_push_int, 456),
                (lua_call, "@is123", 1),
                (display_message, "[url=home.php?mod=space&uid=163977]@Yes[/url] it is."),
(else_try),
                (display_message, "@Nah"),
(try_end),



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

其他WSE LUA API函数 - 这些对高级用户可能有用,但即使从未使用过它们,你也应该能顺利使用。

game.execOperation(strOperationName, arg1, arg2, ....) - 参见操作
game.getOperationFlags(strOperationName) - 标志:None = 0x0,Lhs = 0x1,Cf = 0x2(这是WSE通过读取header_operations.py得出的结论,引擎可能并不认同)。
game.getReg(intTypeId, intIndex)函数中,typeIds的取值为:0表示整数(int),1表示字符串(str),2表示位置(pos)
game.setReg(intTypeId, intIndex, val) (游戏.设置寄存器(整数类型ID, 整数索引, 值)  ,这里是根据函数功能推测的较为贴合语境的翻译,具体含义需结合实际代码逻辑确定 )
游戏获取全局变量(键)
game.setGvar(键, 值)


[/spoiler]

[spoiler=其他杂项]

全局变量
`tableShallowCopy(t, copyMetatable)` - 返回表 `t` 的一个副本,但是任何对象项仍将引用相同的对象。如果 `copyMetatable` 不为 `nil`,则返回的表将具有与 `t` 相同的元表,就像由 `getmetatable(t)` 返回的那样。
`tableRecursiveCopy(t, copyMetatable)` - 返回表 `t` 的副本。`t` 及其“树形结构”下的所有表项都将是实际的值副本,而非引用副本。如果 `copyMetatable` 不为 `nil`,则所有元表都将按照 `getmetatable(t)` 的方式进行值复制。
vector3 - 用于存储和操作三维向量的类
向量3.原型
 

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]) - 构造函数。obj;可用于指定初始值。

向量3运算符 + - * ==

示例:
 

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]) - 递归地打印整个表格。对调试很有用。
它会确保表a.b存在并返回该表,但不会覆盖任何已存在的内容。
以(字符串,起始内容)开头
ends_with(字符串, 结尾)
圆(num, numDecimal)
getTime() 获取自程序启动以来的时间(以毫秒为单位),使用 std::chrono::steady_clock
_print(str) - 在游戏中显示str(专用服务器上的控制台)。常规的print() 使用此函数。
_log(str) - 将str添加到rgl_log.txt中
错误代码可以是:ERROR_ALREADY_EXISTS = 0xB7,ERROR_PATH_NOT_FOUND = 0x3
错误代码:不确定,查看此页面。
迭代器将返回文件名、文件属性
 

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启动代码(sourceforge) - 这段代码会在main.lua之前立即执行。

其他库
完整代码可在此处找到。
LSQLite3 v0.9.6(sqlite 3.24.0)库满足您的数据库需求。sqlite3 = require("lsqlite3")。
lua-std-regex v1.0(适用于C++正则表达式的绑定,比lua的更好)regex = require("regex")。
mobDebug,参见“调试器”章节。

调试器,分析器

什么是调试器?
- 你看不到modsys或C++代码。

安装
获取 ZeroBrane IDE
启动它,选择“项目” -> “项目目录” -> “选择” 并选中你的Lua文件夹
同时启用“项目”->“启动调试器服务器”
ZeroBrane 正在等待传入连接。若要开始调试,请在脚本中运行 local mob = require("mobDebug")。
应该会出现一条消息:*** LUA 调试器已加载 ***
现在:调用 `mob.start()`。游戏将暂停并尝试连接到 ZeroBrane。你也可以向 `start()` 传递一个地址和端口。
如果成功,你应该会在ZeroBrane中看到一条消息。游戏最初处于暂停状态,以便你设置断点。点击绿色箭头继续。
提示:启用监视/堆栈窗口(视图)
就是这样!看起来它不会检测游戏何时结束,所以请手动停止调试器。还有一个VSCode扩展,但我没有测试过。最后,调试会使Lua的运行速度稍微变慢,所以在发布代码时不要让调试功能处于启用状态。

更多信息:ZeroBrane 文档 mobDebug Github


性能分析器
我所做的不过是确认它能正常运行。对我来说,`require("jit.p")` 产生了一个错误,但 `require("jit.profile").start("f", print)` 给出了一些输出。(不要停止它,它会收集数据并定期打印。)

官方文档

沙盒
变更包括:

禁用了package.loadlib、package.cpath、io.popen、os.execute、os.getenv、os.tmpname、ffi库、加载字节码(可能被利用)

提示:通过使用 %storage%\\..\\[其他模块]\\somefile.txt 访问其他模块(这仅适用于 %storage%)。


[/spoiler]

[spoiler=示例]
其中一些是从较大的文件中提取出来的,未经测试,所以要小心。
简单的自定义日志
需要在msfiles文件夹中有header_triggers.py文件。
 

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



小队生成器
该函数在指定位置的正方形区域内生成智能体。
 

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



你可以像这样调用它:
 

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



在服务器控制台中,输入reloadMain时,快速重新加载main.lua
这一切所做的只是调用dofile。请注意,这并不等同于完全重启。
 

("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),
]),



事件管理器
需要msfiles文件夹中的header_triggers.py、ID_scenes.py和ID_items.py。
 

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



超时
延迟执行modssys脚本或lua函数。使用之前的事件管理器。
 

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示例:
 

(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



地图脚本
需要在msfiles文件夹中有ID_scenes.py文件。
 

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)



示例: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)



带有时钟和白色中心点(用于瞄准)的展示界面
需要msfiles中的header_triggers.py、header_presentations.py、ID_meshes.py。
 

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)



在地图开始时,在每个`apple_a`处生成`banhammer`,且`varno1`等于1
使用event_mgr,需要msfiles中的header_triggers.py、ID_items.py。
 

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)



奥利弗兰的战斗AI(多线程)
这是奥利弗兰在2017年制作的。我自己从未运行过这个程序,也没有详细审查过代码,但结果不言而喻。

原始描述:
 

我是一名长期的竞技玩家,一直以来都非常享受单人游戏的各个方面。但随着我的技术提高,由于我随着竞技环境的技术水平一同成长,游戏中的人工智能很快就变得无趣了。鉴于这款游戏在研究机制、剖析游戏风格以及团队协作玩法方面已经达到了一个相对停滞的阶段,我认为我可以利用这些知识打造一个更像玩家的人工智能,它既能在单人游戏中发挥作用,也能在多人游戏中(当然,不是在竞技层面)派上用场。



视频(YouTube):

代码
论坛帖子

代码摘录,专用服务器
我为一个服务器编写的部分Lua代码。包括一个统计数据库和动画辅助工具。

Github



[/spoiler]


 

全部评论