|
发表于 2011-9-14 18:35:31
|
显示全部楼层
比 zMUD 的 #wait 更强大的命令
很多用过 MUSHclient 的人都会遇到一个难题,那就是如何在脚本中暂停一段时间,然后再继续运行,就像 zMUD 中的 wait 命令一样。
例如:
Send("命令1")
-- 等待 2 秒
Send("命令2")
-- 等待 3 秒
Send("命令3")
在很长一段时间里,MUSHclient 都无法解决这个问题,偶尔会有人使用循环来达到这个目的,但是效果却不是很理想:
Send ("命令1")
-- 循环以延迟1秒
For i = 1 to 100000
Next
Send ("命令2")
这个循环的效果是很糟糕的,因为在循环过程中,计算机的 CPU 会完全被占用,你将不能做任何其它的事情。另一个可能发生的问题是延迟根本就没有起作用,因为通过网络发送数据是需要一段时间的,循环命令开始占用 CPU 的时候,第一条命令可能还没有来得及发送出去。开始循环时,CPU 没有空余的时间来处理数据的发送操作,因此只有循环结束后,第一条命令才会继续发送,但是这是又会立即执行第二条命令,所以整个过程循环可能根本就没有起到延迟作用。
由于以上原因,我们需要一种更好的方法来使脚本暂停一段时间。
MUSHclient在新版本中开始支持 Lua脚本,通过 Lua,这个问题终于得到了解决。下面的脚本显示了解决这个问题的方法。你需要把这些脚本写入你的脚本文件里面,以便所有需要暂停时间的触发器和别名调用(共享)。
实现这个功能的基本思路是使用 Lua 的线程模式。我们首先定义一个协同程序(co-routines)或者线程(threads),它可以在需要暂停的时候把自己挂起(暂停),然后把执行权限交还给 MUSHclient。
然后我们再制作一个定时器在希望的延迟时间以后把挂起的线程恢复运行。
以下是需要共享的脚本:
* 一个储存线程的表(Table),通过定时器的名称来控制
* 一个在定时器触发时被调用的函数,它的作用是恢复线程的运行
* 一个“等待”脚本,当我们需要暂停时调用它
等待脚本包括:
* 生成一个唯一的定时器名称供线程表使用
* 以这个唯一的名称制作一个在指定时间后触发的定时器
* 把这个定时器名称和线程地址添加到线程表中
* 使用函数 yield 暂停线程
-- 线程表,储存正在暂停的线程
wait_table = {}
-- 被定时器调用以恢复一个暂停的线程
function wait_timer_resume(name)
thread = wait_table[name]
if thread then
assert(coroutine.resume (thread))
end -- if
end -- function wait_timer_resume
-- 在脚本中调用这个函数来暂停当前的线程
function wait(thread, seconds)
id = "wait_timer_" .. GetUniqueNumber ()
hours = math.floor(seconds / 3600)
seconds = seconds - (hours * 3600)
minutes = math.floor(seconds / 60)
seconds = seconds - (minutes * 60)
status = AddTimer (id, hours, minutes, seconds, "",
timer_flag.Enabled + timer_flag.OneShot +
timer_flag.Temporary + timer_flag.Replace,
"wait_timer_resume")
assert(status == error_code.eOK, error_desc[status])
wait_table[id] = thread
coroutine.yield()
end -- function wait
有了以上的代码后,我们需要制作一个需要使用到暂停功能的别名,总共有两步。为了获得一个需要被挂起的线程,我们需要一个线程首先被执行。真正的 MUD 命令都是在函数 my_alias_thread 中被执行的,它需要由一个别名来调用执行。普通的别名函数一般有三个参数,不过它需要额外的第四个参数,那就是线程地址。这个函数看起来很简单,你可以发现它在执行过程中暂停了3次(调用 wait 函数)。我们需要把当前线程的地址传递给 wait 函数,以便于它知道是哪一个线程需要恢复。
function my_alias_thread (thread, name, line, wildcards)
Note("注释1")
wait(thread, 1)
Note("注释2l")
wait(thread, 2)
Note("注释3")
wait(thread, 3)
Note("完成")
end -- function my_alias_thread
最后,再制作一个简单的函数供别名调用。它的作用是创建一个协同程序,即线程(其实就是上面的 my_alias_thread 函数),然后开始运行它。单词“thread”在恢复运行函数(coroutine.resume)的参数里出现了两次,第一个告诉 resume 函数应该恢复哪一个线程,第二个则传递给被恢复的线程自身,线程暂停的时候需要用到这个“thread“(传递给 wait 函数)。assert 函数的作用是捕捉错误,因为 resumed 函数不会抛出恢复过程中发生的错误而是返回出错的代码,所以这里用 assert 捕捉出现的错误并代为抛出。
function my_alias (name, line, wildcards)
thread = coroutine.create (my_alias_thread)
assert(coroutine.resume (thread, thread, name, line, wildcards))
end -- function my_alias
最后制作一个别名来测试它
name="test_alias"
script="my_alias"
match="test"
enabled="y"
sequence="100"
>
如果别名(或者触发器)的发送目标不是 MUD 服务器,而是脚本引擎时,你仍然可以使用这些暂停函数。你的脚本文件同样需要线程表,wait_timer_resume 和 wait 函数。但是接下来你可以使用一个匿名函数来创建一个线程。
下面是一个例子。你可以把你希望执行的命令放在匿名函数中,即以粗体标示的两行之间。
match="test2"
enabled="y"
send_to="12"
sequence="100"
>
do local t = coroutine.create(function (t)
Note("prepare heal")
wait(t, 1)
Note("cast heal")
wait(t, 2)
Note("eat bread")
wait(t, 3)
Note("Done")
end)
assert(coroutine.resume (t, t)) end
暂停脚本的执行,直到从服务器接收到特定信息为止
前面讲了如何在脚本中实现暂停一段时间的功能,现在让我们在这个基础上实现另一个更难,也更有用的暂停功能 - 暂停脚本的执行,直到从服务器接收到特定的信息为止。当然,要实现这个功能,象下面这样一个简单的函数是不可能实现的:
Send("命令1")
-- 等待从服务器传送特定信息过来
Send("命令2")
这是因为一个函数在结束以前是不能再处理从服务器传送过来的新信息。但是我们可以使用前面类是的方法,即使用 yield 暂停执行当前线程,直到服务器传送过来我们需要的信息,然后再继续运行程序。要实现这个功能,我们需要做三件事:
*一个储存了正在暂停的线程的表,关键字为触发器的名称(可以和前面的定时器线程表使用同一个表)
*一个在触发器被触发时调用的函数,这个函数的作用是恢复暂停线程的运行,并删除这个触发器
*一个“waitfor"函数,它在我们需要暂停的时候被调用。我们需要制作两个类似的函数,waitfor(普通触发器)和waitforregexp(支持正则表达式的触发器)。
waitforregexp 函数的作用如下:
* 生成一个唯一的字符串,这个字符串主要用于在线程表中标识被暂停的线程
* 以这个唯一的字符串为名称制作一个匹配特定内容的触发器
* 把触发器的名称和线程地址添加到线程表中
* 使用函数 yield 暂停线程
-- 线程表,储存正在暂停的线程
wait_table = {}
-- 被触发器调用以恢复一个暂停的线程
function wait_trigger_resume(name, line, wildcards)
EnableTrigger(name, false) -- 禁止这个触发器, 避免重复匹配
DoAfterSpecial (1, "DeleteTrigger ('" .. name .. "')", 12) -- delete it
thread = wait_table[name]
if thread then
assert(coroutine.resume (thread, line, wildcards))
end
end -- function wait_trigger_resume
-- 在脚本中调用这个函数暂停到指定行出现为止(支持正则表达式)
function waitforregexp(thread, regexp)
id = "wait_trigger_" .. GetUniqueNumber()
status = AddTrigger (id, regexp, "",
trigger_flag.Enabled + trigger_flag.RegularExpression +
trigger_flag.Temporary + trigger_flag.Replace,
custom_colour.NoChange,
0, "", -- 要复制的通配符内容, 声音文件的路径
"wait_trigger_resume")
assert(status == error_code.eOK, error_desc[status])
wait_table[id] = thread
return coroutine.yield() -- 返回触发行的 line, wildcards 参数
end -- function waitforregexp
-- 在脚本中调用这个函数暂停到指定行出现为止(不支持正则表达式)
function waitfor(thread, match)
return waitforregexp(thread, MakeRegularExpression(match)) -- 把普通触发内容转换成正则表达式
end -- function waitfor
让我们来看一个使用上面这些函数的例子,假设我们向师傅学习武功,然后判断学习是否成功。要实现这个功能,我们需要捕捉学习后的描述,就像下面这样:
waitforregexp(t, "^(你听了(.+?)的指导,似乎有些心得。|然而你今天太累了,无法再进行任何学习了。)$")
现在的问题是,我们我们如何判断学习结果呢?假设结果有以下两种:
* 成功 - 你听了师傅的指导,似乎有些心得。
* 失败 - 然而你今天太累了,无法再进行任何学习了。
如果是第一种情况,我们可以继续学习下去,如果是第二种情况,说明你的精气太少了,需要休息一下再学习。
幸运的是,我们可以很容易就判断出来。函数 wait_trigger_resume 可以把匹配的行和所有的通配符内容(这些信息都来源于触发行传递的参数)传递过来。因此,我们可以扑获这些信息并作出判断:
line, wildcards = waitforregexp(t,
"^(你听了(.+?)的指导,似乎有些心得。|然而你今天太累了,无法再进行任何学习了。)$")
现在我们可以判断整行内容,或者某些通配符的内容,并作出正确的判断。
为了方便使用,我们把上面所有的脚本放在一起,并制作一个别名去调用它:
function alias_thread(t, name, line, wildcards)
repeat
Send("learn master force 1")
local trigger_line, trigger_wildcards = waitforregexp(t,
"^(你听了(.+?)的指导,似乎有些心得。|然而你今天太累了,无法再进行任何学习了。)$")
until trigger_line == "然而你今天太累了,无法再进行任何学习了。"
Send("sleep")
end -- function alias_thread
function alias(name, line, wildcards)
thread = coroutine.create(alias_thread)
assert(coroutine.resume(thread, thread, name, line, wildcards))
end -- function alias
这个例子只是一个很简单的事例,从中你可以看出,平常需要几个触发器才能完成的功能,使用上面的方法只需要一个函数就可以实现。
最后,我们还可以简化一下,把别名调用的函数和真正实现命令的函数结合在一起
match="begin_learn"
enabled="y"
echo_alias="y"
regexp="n"
send_to="12"
sequence="100"
>
do local t = coroutine.create(function (t)
repeat
Send("learn master force 1")
line, wildcards = waitforregexp (t,
"^(你听了(.+?)的指导,似乎有些心得。|然而你今天太累了,无法再进行任何学习了。)$")
until line == "然而你今天太累了,无法再进行任何学习了。"
Send("sleep")
end)
assert (coroutine.resume (t, t)) end
|
|