骑马与砍杀中文站论坛

标题: 如何使用WSE/lua(该教程已翻译成中文) [打印本页]

作者: 偃靖    时间: 2025-6-14 16:31
标题: 如何使用WSE/lua(该教程已翻译成中文)
本帖最后由 偃靖 于 2025-6-16 09:01 编辑

写在前面:
新三年,旧三年,缝缝补补又三,说的就是战团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代码
没有令人困惑的定点乘法器,简单的(向量)数学运算

而不是
  1. (store_mul, ":area", ":r", ":r"),
  2. (val_mul, ":area", 314159),
  3. (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访问游戏。

操作(operation)

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

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

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

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

返回值为:
  1. boolDidNotFail, intError                                                = game.cf_operation(arg1, arg2, ...) for can_fail operations
  2. intResult, intError                                                                        = game.lhs_operation(lhs_arg, arg1, arg2, ...) for lhs operations
  3. boolDidNotFail, intResult, intError        = game.cf_lhs_operation(lhs_arg, arg1, arg2, ...) for operations which are both can_fail and lhs
  4. intError                                                                                                                = game.operation(arg1, arg2, ...) for all other operations

  5. (lhs = left hand side? Operations that assign a value, e.g. val_add)
  6. (cf  = can fail, e.g. agent_is_active)对于可能失败的操作:
  7. `boolDidNotFail`(操作是否未失败), `intError`(错误码) = `game.cf_operation(arg1, arg2, ...)`

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

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

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

  14. (lhs = 左值?指赋值操作,例如 `val_add`)
  15. (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:
  1. local val = game.agent_get_slot(0, agent_id, slot_no)
  2. local val = game.op.agent_get_slot(agent_id, slot_no)
复制代码

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

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


寄存器,全局变量(寄存器:reg, s, pos, 全局变量:$开头的变量)

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

示例:
  1. game.reg[0] = 123
  2. local i = game.reg[0]
  3. game.sreg[0] = "test string"
  4. local s = game.sreg[0]
  5. game.preg[0] = game.pos.new()
  6. local pos = game.preg[0]
复制代码

截至当前版本,寄存器是按值复制而非按引用复制。因此,像这样操作:
  1. game.preg[0]:rotX(30)
复制代码

不会更改实际的游戏寄存器。这一点可能会在未来版本中得到改进。目前你需要做的是:
  1. local p = game.preg[0]
  2. p:rotX(30)
  3. game.preg[0] = p
复制代码

要访问全局变量,请使用game.gvar.gvar_name
  1. game.gvar.g_encountered_party = 123
  2. print(game.gvar.g_encountered_party)
复制代码

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

常量

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

截至当前版本,只有与以下两个正则表达式之一匹配的行才会被接受:
  1. ^(\w+)=(((-)?0x[\da-fA-F]+)|((-)?\d+(\.\d+)?))$
  2. ^(\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添加一个死斗触发条件。不要这样写:
  1. game.addTrigger("mst_multiplayer_dm", -28, 0, 0, condCB)
复制代码

你可以将header_triggers.py复制到你的msfiles文件夹中,然后执行:
  1. game.addTrigger("mst_multiplayer_dm", game.const.ti_on_agent_hit, 0, 0, condCB)
  2. --or if you like to be specific:
  3. 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。

示例:
  1. function condCB()
  2.         doStuff()
  3.         return true
  4. end
  5. 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)
示例:
  1. game.addItemTrigger("itm_fighting_pick", game.const.ti_on_weapon_attack, function()
  2.         print("agent no " .. game.store_trigger_param_1(0) .. " swung a fighting pick")
  3. end)
复制代码


场景道具触发器:
index   = game.addScenePropTrigger(场景道具ID|场景道具编号, 触发间隔时间, 回调函数)
success = game.removeScenePropTrigger(strPropID|intPropNo, index)  # 成功 = game.removeScenePropTrigger(字符串道具ID|整数道具编号, 索引)
num     = game.getNumScenePropTriggers(strPropID|intPropNo)
示例:
  1. game.addScenePropTrigger(game.const.spr_apple_a, game.const.ti_on_scene_prop_init, function()
  2.         print("Apple " .. game.store_trigger_param_1(0) .. " initialized.")
  3. end)
复制代码


世界触发器:

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

这些的工作方式与模块系统迭代器完全相同,并接受相同的参数。
  1. game.partiesIt()
  2. game.agentsIt([pos, radius, [use_grid]]) --pos: integer for pos register or table of type game.pos
  3. game.propInstIt([subKindNo], [metaType]) --a.k.a. [object_id], [object_type]
  4. game.playersIt([skip_server])
复制代码

示例:
  1. for curPlayer in game.playersIt(true) do --skip server. You can also use 1 or 0
  2.         foo(curPlayer)
  3. end
复制代码


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

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

MS乘数本质上是一种指定当前长度单位的方法。
  1. set_fixed_point_multiplier(1),
  2. position_get_x(":x", 0), #get pos0_x in meters
  3. set_fixed_point_multiplier(100),
  4. position_get_x(":x", 0), #get pos0_x in centimeters
复制代码

在Lua中,情况更简单,你使用浮点数。“1.0”始终表示1米,“0.01”始终表示1厘米。
  1. --this is equivalent to above modsys example
  2. print(game.preg[0].o.x) --pos0_x float
复制代码

位置旋转

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

游戏。旋转。原型
  1. s = vector3.new({x = 1}) --x axis
  2. f = vector3.new({y = 1}) --forwards/y axis
  3. u = vector3.new({z = 1})  --up/z axis
  4. function getRot(self) --returns vector3.new({z = yaw, x = pitch, y = roll})
  5. function rotX(self, angle) --rotate around x axis, angle in degrees
  6. function rotY(self, angle) --rotate around y axis, angle in degrees
  7. function rotZ(self, angle) --rotate around z axis, angle in degrees
  8. function rotate(self, rotVec3) --rotate around all axis, zxy order, angle in degrees
复制代码


game.pos

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

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

  3. --see game.rotation.prototype for these
  4. function getRot(self)
  5. function rotX(self, angle)
  6. function rotY(self, angle)
  7. function rotZ(self, angle)
  8. function rotate(self, rotVec3)
  9. function moveX(self, val) --Move by val into local X direction
  10. function moveY(self, val) --Move by val into local Y direction
  11. function moveZ(self, val) --Move by val into local Z direction
  12. function move(self, val) --Move by val into local directions, val must be table with any of x,y,z entries
  13. function dist(self, pos2) --returns distance between self.o and pos2.o
  14. function isBehind(self, pos2) --returns true if self is behind pos2 (shorthand for game.position_is_behind_position)
复制代码


示例:
  1. local pos0 = game.preg[0]
  2. local newRot = game.rotation.new(pos0.rot)
  3. local newPos = game.pos.new({rot = newRot})
  4. newPos:rotX(45)
  5. newPos:rotate({x = 90, z = 180})
  6. newPos:move({x = 10, z = 5})
复制代码


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

tablePrsnt必须采用以下格式

  1. tablePrsnt = {
  2.         id = str,
  3.         [flags] = {int, int, ...},
  4.         [mesh] = int,
  5.         triggers = {[numTriggerConst] = func, ...}
  6. }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文件夹中,你可以使用在那里定义的触发器值。

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

示例:
  1. --copy header_presentations.py, header_triggers.py and ID_meshes.py to your msfiles folder for this example.
  2. local index = game.addPrsnt({
  3.         id = "myPrsnt",
  4.         flags = {game.const.prsntf_read_only, game.const.prsntf_manual_end_only},
  5.         --you can initialize an array like this, without keys - they don't matter here anyway.
  6.         mesh = game.const.mesh_cb_ui_main,
  7.         triggers = {
  8.                         [game.const.ti_on_presentation_load] = function ()
  9.                                 --The const inside the [] declares a number key, similar to keyName = 123 for string keys.
  10.                                 game.presentation_set_duration(9999999)
  11.                                 local overlay = game.create_mesh_overlay(0, game.const.mesh_mp_ingame_menu)
  12.         
  13.                                 local position = game.pos.new()
  14.                                 position.o.x = 0.3
  15.                                 position.o.y = 0.3
  16.                                 game.overlay_set_position(overlay, position)
  17.         
  18.                                 position.o.x = 0.5
  19.                                 position.o.y = 0.8
  20.                                 game.overlay_set_size(overlay, position)
  21.                         end
  22.         }
  23. })
  24. game.start_presentation(index)
复制代码


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

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

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

粒子系统(particle system)

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

tablePsys必须采用以下格式

  1. tablePsys = {
  2. id = 字符串, [flags] = {整数, 整数, ...},
  3. mesh = 字符串|整数,num_particles = 整数, life = 数字, damping = 数字, gravity_strength = 数字, turbulance_size = 数字, turbulance_strength = 数字,
  4. alpha_keys                        = {{数字, 数字}, {数字, 数字}},
  5. red_keys                        = {{数字, 数字}, {数字, 数字}},
  6. green_keys                        = {{数字, 数字}, {数字, 数字}},
  7. blue_keys                        = {{数字, 数字}, {数字, 数字}},
  8. scale_keys                        = {{数字, 数字}, {数字, 数字}},
  9. emit_box_size                = {数字, 数字, 数字},
  10. emit_velocity                = {数字, 数字, 数字},
  11. emit_dir_randomness = 数字,
  12. rotation_speed = 数字,
  13. rotation_damping = 数字}
复制代码


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

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

示例:

  1. local psys = game.addPsys({id = "blabla", flags = {game.const.psf_billboard_3d, game.const.psf_global_emit_dir}, mesh = "prt_mesh_snow_fall_1",
  2.         num_particles = 150,
  3.         life = 2,
  4.         damping = 0.2,
  5.         gravity_strength = 0.1,
  6.         turbulance_size = 30,
  7.         turbulance_strength = 20,
  8.         alpha_keys = {{0.2, 1}, {1, 1}},
  9.         red_keys = {{1.0, 1.0}, {1, 1.0}},
  10.         green_keys = {{1.0, 1.0}, {1, 1.0}},
  11.         blue_keys = {{1.0, 1.0}, {1, 1.0}},
  12.         scale_keys = {{1.0, 1.0},   {1.0, 1.0}},
  13.         emit_box_size = {10, 10, 0.5},
  14.         emit_velocity = {0, 0, -5.0},
  15.         emit_dir_randomness = 1,
  16.         rotation_speed = 200,
  17.         rotation_damping = 0.5
  18. })
  19. game.particle_system_burst(psys, game.preg[0], 20)
复制代码


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

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

模块操作钩子(为operation自定义回调方法)
你可以使用 `game.hookOperation(operation, funcCallback)` 来挂钩模块操作。

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

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

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



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

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

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

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

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

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

简而言之,变体如下:
  1. boolExecute
  2. boolExecute, boolFail
  3. boolExecute, numLhs
  4. boolExecute, boolFail, numLhs boolExecuteboolExecute,boolFailboolExecute,numLhsboolExecute,boolFail,numLhs
复制代码


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

从Lua调用操作时,该钩子不会被触发。不过,这个任务可以通过覆盖游戏元表来实现,其定义如下:
  1. game.mt = {
  2.                 __index = function(table, key)
  3.                                 return function(...)
  4.                                                 return game.execOperation(key,...)
  5.                                 end
  6.                 end
  7. }
复制代码


例如,可以执行(尚未完成):
  1. game.mt = {
  2.                 __index = function(table, key)
  3.                                 If key == "player_get_gold" then
  4.                                                 return myCustomGoldCalculation
  5.                                                 --return function that takes all args (...) and returns gold
  6.                                                 --this function should, if neccessary, use game.execOperation("player_get_gold", args)
  7.                                                 --instead of game.player_get_gold(args), for avoiding an infinite loop.
  8.                                 end
  9.                                 return function(...)
  10.                                                 return game.execOperation(key,...)
  11.                                 end
  12.                 end
  13. }
  14. setmetatable(game, game.mt)
复制代码


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

脚本钩子(为script自定义回调方法)
你可以使用game.hookScript(script_no, funcCallback)挂钩脚本。

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

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

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



示例:
  1. local a = 0
  2. game.hookScript(game.script.game_get_console_command, function(...)
  3.   print("Hook")
  4.   a = a + 1

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

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

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

  14.   elseif a == 15 then
  15.     game.hookScript(game.script.game_get_console_command, nil)
  16.   end
  17. end)
复制代码


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

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

示例:
  1. local no = game.getScriptNo("game_start")
  2. game.call_script(no)
复制代码


WSE更新现在提供了一个包装器:game.script.[名称]
  1. 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操作都可能导致的那样。

示例:
  1. (try_begin),
  2.                 (lua_push_int, 456),
  3.                 (lua_call, "@is123", 1),
  4.                 (display_message, "[url=home.php?mod=space&uid=163977]@Yes[/url] it is."),
  5. (else_try),
  6.                 (display_message, "@Nah"),
  7. (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.原型
  1. x = 0
  2. y = 0
  3. z = 0
  4. --You can also use vec3[1], vec3[2], vec3[3] instead of x,y,z
  5. function len(self) --returns the length of the vector
  6. function dist(self, vec2) --returns the distance between self and vec2
  7. function dot(self, vec2) --returns the dot product of self and vec2
  8. function cross(self, vec2) --returns the cross product of self and vec2
  9. function unit(self) --returns a normalized copy of itself
  10. 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运算符 + - * ==

示例:
  1. local vecA = vector3.new({x = 60, y = 30})
  2. local vecB = vector3.new()

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

  4. if vecA == vecB then
  5.         --will not happen
  6. end

  7. vecA = vecA * vecB --{60*0, 30*0, 0*0}
  8. if vecA == vecB then
  9.         --will happen
  10. 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
错误代码:不确定,查看此页面。
迭代器将返回文件名、文件属性
  1. for path, attr in lsdir("dirtest") do
  2.         print(path, attr)
  3. end

  4. --Might be good to check for errors first
  5. local iter, err_msg = lsdir("")
  6. if iter then
  7.         for path, attr in iter do
  8.                 print(path, attr)
  9.         end
  10. else
  11.         print(err_msg)
  12. 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文件。
  1. myLog = io.open("custom_log.txt", "a")
  2. myLog:write("Server started at " .. os.date("%Y.%m.%d, %X") .. "\\n")
  3. myLog:flush()

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

  7. function playerJoinedCallback()
  8.                 local playerNo = getTriggerParam(1)
  9.         
  10.                 game.str_store_player_username(0, playerNo)
  11.                 game.str_store_player_ip(1, playerNo)
  12.         
  13.                 myLog:write(game.sreg[0] .. " joined with IP " .. game.sreg[1] ..
  14.                                         " at " .. os.date("%Y.%m.%d, %X") .. "\\n")
  15.                 myLog:flush()
  16.                 return false
  17. end

  18. templates = {
  19.                 "dm",
  20.                 "tdm",
  21.                 "cf", --capture the flag
  22.                 "sg", --siege
  23.                 "bt", --battle
  24.                 "fd", --fight and destroy
  25.                 "ccoop", --invasion
  26.                 "duel"
  27. }

  28. --add triggers to all multiplayer templates
  29. for k,v in pairs(templates) do
  30.         game.addTrigger("mst_multiplayer_" .. v, game.const.ti_server_player_joined, 0, 0, playerJoinedCallback)
  31. end
复制代码


小队生成器
该函数在指定位置的正方形区域内生成智能体。
  1. function spawnSquad(troop, amount, position)
  2.                 local sideLen = math.floor(math.sqrt(amount))
  3.                 local leftovers = amount - sideLen * sideLen
  4.                 local spawnSpreadDistance = 1
  5.                 position.o.x = position.o.x - math.floor(sideLen/2) * spawnSpreadDistance
  6.                 position.o.y = position.o.y - math.floor(sideLen/2) * spawnSpreadDistance
  7.         
  8.                 for i=1, sideLen do
  9.                                 for j=1, sideLen do
  10.                                                 game.set_spawn_position(position)
  11.                                                 game.spawn_agent(troop)
  12.                                                 position.o.x = position.o.x + spawnSpreadDistance
  13.                                 end
  14.                                 position.o.x = position.o.x - spawnSpreadDistance * sideLen
  15.                                 position.o.y = position.o.y + spawnSpreadDistance
  16.                 end
  17. end
复制代码


你可以像这样调用它:
  1. local pos = game.pos.new({o = {x=100,y=100}})
  2. spawnSquad(game.const.trp_bandit, 10, pos)
复制代码


在服务器控制台中,输入reloadMain时,快速重新加载main.lua
这一切所做的只是调用dofile。请注意,这并不等同于完全重启。
  1. ("wse_console_command_received", [ #this is a script that WSE adds
  2.                 (store_script_param, ":command_type", 1),
  3.         
  4.                 (str_equals, s0, "@reloadMain"),
  5.                 (lua_push_str, "@main.lua"),
  6.                 (lua_call, "@dofile", 1), #one param, the string we pushed
  7.                 (set_trigger_result, 1),
  8. ]),
复制代码


事件管理器
需要msfiles文件夹中的header_triggers.py、ID_scenes.py和ID_items.py。
  1. --[[
  2. Usage:
  3.         event_mgr.subscribe(event_id, callback)
  4.         event_id:
  5.                 "ti_constant", e.g. "ti_before_mission_start"
  6.                 "timer_x.y", e.g. "timer_1.5" or "timer_0"
  7.                 "itm_a:ti_constant", e.g. "itm_french_cav_pistol:ti_on_weapon_attack"
  8.                 "spr_b:ti_constant", same thing
  9.                 "script_", e.g. "script_game_quick_start" (versus hookScript - you can not control execution of modsys script here)
  10.                 "key_", e.g.:
  11.                 "key_o"                              O clicked
  12.                 "key_o down=key_shift"               O clicked while Shift down
  13.                 "key_k down=key_shift key_control"   key_shift, key_control means both left/right
  14.                 "your_own_event_id", can be used with dispatch()

  15.         returns: a subscription ID which you can use with unsubscribe

  16.         event_mgr.unsubscribe(event_id, subscription_id)

  17.         event_mgr.clear()
  18.         Clear all callbacks. Useful for hot-reloading
  19.         Example reload:
  20.         event_mgr.subscribe("key_r down=key_shift", function()
  21.                 event_mgr.clear()
  22.                 print("Reloading")
  23.                 dofile("main.lua")
  24.         end)

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

  27. local regex = require "regex"

  28. if not event_mgr then
  29.         event_mgr = {
  30.                 events = {}
  31.         }
  32. end

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

  35.         local i = 1
  36.         while event_mgr.events[event_id][i] ~= nil do i = i + 1 end
  37.         event_mgr.events[event_id][i] = callback
  38.         return i
  39. end

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

  43. function event_mgr.dispatch(event_id, ...)
  44.         if event_mgr.events[event_id] then
  45.                 for _, event_callback in pairs(event_mgr.events[event_id]) do
  46.                         event_callback(...)
  47.                 end
  48.         end
  49. end

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

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

  58.         local function add_mst_trig(const, callback)
  59.                 for i = 0, game.getNumTemplates()-1 do
  60.                         game.addTrigger(i, const, 0, 0, callback)
  61.                 end
  62.         end
  63.         
  64.         --generic dispatcher
  65.         local function cb()
  66.                 event_mgr.dispatch(event_id)
  67.                 return false
  68.         end

  69.         if starts_with(event_id, "ti_") then
  70.                 local const = game.const.triggers[event_id]
  71.                 add_mst_trig(const, cb)
  72.         
  73.         elseif starts_with(event_id, "timer_") then
  74.                 local const = tonumber(string.match(event_id, "%d+%.?%d*"))
  75.                 add_mst_trig(const, cb)

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

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

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

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

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

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

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

  105.                         else
  106.                                 local k = game.const.triggers[keyname]
  107.                                 return function() return op(k) end
  108.                         end
  109.                 end

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

  114.                 add_mst_trig(0, function()
  115.                         if clicked() then
  116.                                 for i = 1, #down do
  117.                                         if not down[i]() then return false end
  118.                                 end

  119.                                 event_mgr.dispatch(event_id)
  120.                                 return false
  121.                         end
  122.                 end)
  123.         end
  124. end

  125. --For calling from modsys
  126. function eventDispatch(event_id, ...)
  127.         event_mgr.dispatch(event_id, ...)
  128. end
复制代码


超时
延迟执行modssys脚本或lua函数。使用之前的事件管理器。
  1. local timeouts = {}

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

  5.                 for i = #timeouts, 1, -1 do
  6.                         if timeouts[i].t <= t_now then
  7.                                 timeouts[i].cb(timeouts[i].cb, t_now)
  8.                                 table.remove(timeouts, i)
  9.                         end
  10.                 end
  11.         end
  12. end

  13. function timeoutAddScript(time, script_no, ...)
  14.         local args = {...}
  15.         local cb = function()
  16.                 game.call_script(script_no, unpack(args))
  17.         end

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

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

  23. event_mgr.subscribe("ti_before_mission_start", function() timeouts = {} end)
  24. event_mgr.subscribe("timer_0", timeoutsTick)
复制代码


Modsys示例:
  1. (lua_push_int, 2000),
  2. (lua_push_int, "script_for_timeout"),
  3. (lua_push_int, ":var1"),
  4. (lua_push_int, ":var2"),

  5. (lua_call, "@timeoutAddScript", 4),
  6. (lua_set_top, 0), #Just make sure its cleaned up
复制代码


地图脚本
需要在msfiles文件夹中有ID_scenes.py文件。
  1. local subbed_events = {}

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

  11.       unsubscribe = function(event_id, index)
  12.         for i,v in ipairs(subbed_events) do
  13.           if v.idx == index then
  14.             table.remove(subbed_events, i)
  15.             return _G.event_mgr.unsubscribe(event_id, index)
  16.           end
  17.         end
  18.       end,

  19.       dispatch = _G.event_mgr.dispatch
  20.     }   
  21.   }, {__index = _G})
  22. }

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

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

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

  32.   local fenv = {}
  33.   setmetatable(fenv, fenv_mt)

  34.   setfenv(func, fenv)
  35.   func()
  36. end

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

  43.   local scene = game.store_current_scene(0)
  44.   load_map_script(scene_names[scene])
  45. end)
复制代码


示例:lua/map_scripts/scn_mp_custom_map_3.lua

  1. print("custom_map_3 script loaded")

  2. event_mgr.subscribe("spr_apple_a:ti_on_init_scene_prop",
  3.   function()
  4.     print("apple")
  5.     do_something()
  6.   end)

  7. event_mgr.subscribe("itm_birch_trunk:ti_on_weapon_attack",
  8.   function()
  9.     print("attack")
  10.     explode_player()
  11.   end)
复制代码


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

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

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

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

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

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

  19.                         nextRefreshTime = 0

  20.                         midDotOverlay = game.create_mesh_overlay(0, game.const.meshes.mesh_white_dot)
  21.                         screenPos.o.x = 0.5 - 0.0002
  22.                         screenPos.o.y = 0.75/2 - 0.0001
  23.                         sizePos.o.x = 0.1
  24.                         sizePos.o.y = 0.1
  25.                         game.overlay_set_position(midDotOverlay, screenPos)
  26.                         game.overlay_set_size(midDotOverlay, sizePos)
  27.                 end,
  28.                
  29.                 [game.const.ti_on_presentation_run] = function()
  30.                         local curTime = game.store_trigger_param_1(0)
  31.                         if curTime >= nextRefreshTime then
  32.                                 nextRefreshTime = nextRefreshTime + 1000
  33.                                 game.overlay_set_text(clockOverlay, os.date("%X"))
  34.                         end
  35.                 end
  36.         }
  37. })

  38. event_mgr.subscribe("ti_after_mission_start", function()
  39.         game.start_presentation(prsnt)
  40. end)
复制代码


在地图开始时,在每个`apple_a`处生成`banhammer`,且`varno1`等于1
使用event_mgr,需要msfiles中的header_triggers.py、ID_items.py。
  1. event_mgr.subscribe("spr_apple_a:ti_on_init_scene_prop",
  2.   function()
  3.     local inst = game.store_trigger_param_1(0)
  4.     if game.prop_instance_get_variation_id(0, inst) == 1 then
  5.             print("found apple " .. inst)

  6.             game.prop_instance_get_position(1, inst) --pos1 translates to 1
  7.             game.set_spawn_position(1)
  8.             game.spawn_item(game.const.itm_banhammer, 0, 0)
  9.     end
  10.   end)
复制代码


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

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


视频(YouTube):

代码
论坛帖子

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

Github



[/spoiler]




作者: 偃靖    时间: 2025-6-14 17:27
二楼备用
作者: huagao    时间: 2025-6-14 20:26
等我rpg打完
作者: 偃靖    时间: 2025-6-14 20:45
huagao 发表于 2025-6-14 20:26
等我rpg打完

多多交流,搞它一波
作者: zz010606    时间: 2025-6-14 23:00
非常好的帖子,继续继续,加把火
作者: 战争傀儡阿格兰    时间: 2025-6-15 08:25
天书.jpg
作者: zrcs    时间: 2025-6-15 08:54
很多国内MOD不支持可惜了,WSE2千人战很丝滑而且不吃配置,配置好一点2千人也可能实现,可惜攻防城战不行,因为这些城市根本难以站下这么多人,特别是玩儿家守城战出来卡成摇晃机了,眼睛都要瞎的节奏。
作者: 偃靖    时间: 2025-6-15 11:41
战争傀儡阿格兰 发表于 2025-6-15 08:25
天书.jpg

谦虚了,实力派
作者: 偃靖    时间: 2025-6-15 11:42
zrcs 发表于 2025-6-15 08:54
很多国内MOD不支持可惜了,WSE2千人战很丝滑而且不吃配置,配置好一点2千人也可能实现,可惜攻防城战不行, ...

以后战团mod转wse2还能挖掘一波
作者: 英勇的苹果    时间: 2025-6-15 12:55
本帖最后由 英勇的苹果 于 2025-6-15 13:13 编辑

感谢大佬分享,正研究这个~
另外,看官网原帖,1.1.3.6版本出现了新Bug,但作者"K700"大佬正在度假,估计下个月才会更新修复。

PS:LZ大佬,我看我后发现,"game"表标签里的"常量"相关内容,出现了两次"这可能看起来令你望而生畏"字段。
怀疑不仅是重复翻译,且原文本对应的翻译也没有了……


作者: 英勇的苹果    时间: 2025-6-15 13:14
偃靖 发表于 2025-6-15 11:42
以后战团mod转wse2还能挖掘一波

另一方面,新Mod作者在制作攻守城场景时,也敢搞大场面了~
作者: 偃靖    时间: 2025-6-15 14:58
英勇的苹果 发表于 2025-6-15 12:55
感谢大佬分享,正研究这个~
另外,看官网原帖,1.1.3.6版本出现了新Bug,但作者"K700"大佬正在度假,估计下个月 ...

太好了,有啥研究成果,欢迎及时分享啊。
翻译问题,我稍后确认下,随时修正。
作者: 龙静颜    时间: 2025-6-15 18:08
有新进展了呀,辛苦咯
作者: 偃靖    时间: 2025-6-15 21:41
龙静颜 发表于 2025-6-15 18:08
有新进展了呀,辛苦咯

还谈不上。
所以翻成中文介绍一下,等大家一起来攻关。
作者: 偃靖    时间: 2025-6-16 09:08
英勇的苹果 发表于 2025-6-15 12:55
感谢大佬分享,正研究这个~
另外,看官网原帖,1.1.3.6版本出现了新Bug,但作者"K700"大佬正在度假,估计下个月 ...

已经修正。并且修改几处意义容易误解的名词。
作者: a2609931159    时间: 2025-6-22 17:41
非常好的帖子,加油加油




欢迎光临 骑马与砍杀中文站论坛 (https://bbs.mountblade.com.cn/) Powered by Discuz! X3.4