MUD教程--巫师入门教程3

时间:2023-03-09 08:51:23
MUD教程--巫师入门教程3

1. 指令格式为:edit <档名>,只加文件名,默认为当前目录,加here,表示编辑你当前所处的房间, 回车后即进入线上编辑系统。 
2. 如果这是一个已经有的档案,你可以使用 z 或 Z 来看档案。z表示一次显示20行,Z表示一次显示40行。为了编辑方便,最好在开始时用 n 表示每一行在开头处显示它的行数,再用一次 n 取消行数显示; 
3. 还有一种方法 ,就是直接打入行数,则会跳至那行的内容上;
4. 如果你开始没打 n ,却想知道现在是第几行请打 = , 想知道内容请打 p ;
5 如果想直接到档案的结尾可输入 $;
6. 五种编辑命令 a i c m d : 
  a = 从此各行之下插入编辑; 
  i = 从此行之上插入编辑; 
  c = 修改此行并插入编辑;
m = 把本行移到特定的行号去 
d = 删除;
这些命令也可以和行数结合使用。如 : 
  7a = 在第7行后插入编辑;
6i = 在第6行前插入编辑;
4c = 直接编辑第4行;
  5,8m1 = 将第 5~8 行移至原第 1 行之下。 
3d = 删去第 3 行 ; 
  2,10d = 删去第 2~10 行; 
7. 如果这是一个新命名的档案, 这里面当然是没有行数了,一般你应该用 a 来开始编辑第一行;
8. 如果你对某一行或某几行编辑完毕后, 请在编辑结尾的次列开头处打 . 即可退出行编辑状态;
9. 如果想存档请打 x 表示存档退出。 否则请打 q 或 Q 表示放弃编辑退出。 
10.其余功能可打 h 参考。由于有的MUDOS汉化不太好,下面列出 h 的中文注释:

/ 前向查找你后面所跟着的字符,比如/酒袋
? 后向查找你后面所跟着的字符
= 显示当前行是第几行
a 在当前行后新增加一行
A 类似'a'命令,但是将翻转自动缩进模式
c 将当前行内容覆盖掉输入新编辑内容
d 删除指定范围的行
e 退出当前档案, 开始编辑另一档(档案改变过未存盘无效)
E 类似'e'命令,但是文件被修改过也有效
f 显示或改变文件名
g 查找匹配行并对其执行相应命令
h 帮助文件(就是显示你现在看到的这些信息,可能是英文)
i 在当前行前面插入一行
I 排版整个代码 (Qixx version 1.0)
j 合并行,系统默认是将后续行连接到当前行
k 标记当前行- later referenced as 'a
l 显示指定行(可显示控制字符)
m 移动指定行(或几行)到指定位置
n 行号显示切换开关
O 同命令'i'
o 同命令'a'
p 输出指定范围行
q 退出编辑器
Q 退出编辑器,即使文件已经被修改且未存盘
r 在文件尾或指定行后读进另一文件
s 查找并替换(只对当前行第一个查找的字符串进行替换)
set 查询,改变和保存编辑器的设定值
t 复制指定行到指定位置
v 搜索并对不匹配行执行指定命令
x 保存文件并退出
w 将编辑的内容写到当前文件或指定的文件里
W 类似'w'命令,但是是将编辑的内容附加到指定或当前文件后
z 显示20行,可用参数 . + -
Z 显示40行,可用参数 . + -

      前面一章讲过,当你成功地编写了一个程序后,只是意味着这个程序已经在硬盘了,只有在别的使用者调用到它的时候,才会被呼叫出来,进入内存。文件本身是否有错误,将首先会在这时被发现。对于巫师来说,消极地等待别人去调用它时再去发现有没有错是十分不明智的,这时最好的就是先update它。
    update <文档名>
    注意了:如果系统显示:“重新编译 ***.c:成功!”的信息,并不就表示你的这个程序就完全正确了,它只是表示你的程序的基本语法没有错误,如果程序里还有一些由其它的条件或文件才能触发呼叫的函数的话,还有有可能存在一些隐患,保险的做法就是按照设计时的条件把它们一一触发,进行尝试,直到完全通过。比如象一些NPC里有是否接受拜师的函数,你则想法不同的条件的人去拜它试试,把每一种可能都试过,看看是否执行正常。有的房间里加了add_action(),你则一一试试这些add_action(),看一下后果。
      如果文档中有错误,一般系统会唰地一下子出来一大串的错误信息,对于新巫师来说,只要去看看第一行的出错内容就行了,后面的很多错误都是由前面的带来的。还有许多信息还显示其它的与一些重要的系统文件也出错,也大抵是如此,首先还是找出关于这个文档里的第一个出错的行数,再到这个文档里去查找,仔细查看该行数,以及前后几行,有没有少写多写括号、漏记多添逗号、定义变量类型错误等等,如果显示出错的行数在最后一行,甚至更后的话,那就要看看是不是函数声明出错,或者定义了无效的函数。每改一次,再update一次,直至编译成功。有关于各种出错信息的意思和处理办法,还是要在实践中多多尝试,但是,在此要忠告各位新巫师,如果你所工作实习的MUD是一个正在开放中的MUD,希望对于没有任何把握的文件的编译工作最好先在自己的单机版进行,有些恶性的错误严重时会导致整个游戏宕机。

      接下来就是任何一个新巫师一上任就十分感兴趣的命令----call!call就是直接呼叫(执行)函数的意思。在某种程度上,它就象征着巫师手中的神杖。这个对于玩家来说威力无比的功能,既是一种巫师利器,更是一种危险器械。因此,在大多数的MUDLIB中都对于call的命令的使用进行了记录,以备天神的查看和监督。call的命令格式如下:
      call <物件>-><函数>(<参数>, ...... ) 
      从其理论上来说,它可以呼叫任何没有被protect的函数。具体我们可以看这个程序: /adm/daemons/emoted.c d 在这个程序里面有一个这样的函数:
string *query_all_emote() 

      return keys(emote); 

      那么。我们就可使用call命令直接呼叫它:
call /adm/daemons/emoted.c->query_all_emote() 
        ~~~~~~~~~~~~~~~~~~~~~   ~~~~~~~~~~~~~~~
                  (物件)                     (物件的函数) 
      由于这个函数本身没有要传参数,就不用再加参数了。那么执行后,程序本身就会返回一个字符串的数组回来。而显示在我们屏幕上面的内容就是所有的emote的英文名字。 
  在实际工作中,上面的这种用得还是很少的,大部分的修改和查看我们都可以用more或edit去完成,但是对于尤其象玩家档案这些以.o形式储存的文件用edit编辑则有些费劲了,所以这时使用call的命令来得更为方便些。
      巫师们常常会call me(或者id)->set("combat_exp",10000) 
  在这里,me就是自己,其实它对应着一个物件程序:/obj/user.c,后面的set()也是系统放在一个文件里最基本的函数。后面的括号里面便是这个set函数的参数。它的意思就是在me这个物件里执行set()函数,通过set()函数将combat_exp这个参数设为10000。如果。要改变别的人,就可以在call后面加上这个人的id。set()这个函数可以执行什么呢?其实很简单,打开一个复杂一点的NPC,它里面所具有的参数,我们一般都能用call命令进行。
      call命令可以调用的函数非常多,一般由你call的物件有关。但在一般使用中,我们大多使用三种函数,一是set,也就相当于我们做程序中的set一样,你可以set这个物件任何可以set的参数;第二个就是query,用它可以查看你所call的物件有没有这个参数,这个参数内容是什么?第三个就是delete,顾名思义,它正与set相反,用以删除物件上的这个参数。其它一些固定的函数,例如武功的set_skill,设定姓名的set_name等等就不一一叙述了。
一共四章的《新巫师入门手册》写出去以后,叮当一直有一种诚惶诚恐的感觉。因为我无论在接触MUD之前还是之后,都未接触过任何的编程语言学习,更别提什么C了。象我这样的人写出的教材,是否会误人子弟呢?但叮当也相信,在网上,也一定会有许许多多与当初的叮当一样,对于已有的一些巫师教材看得云里雾里的感觉。不是责怪这些教材写得太深,而是确实自己的基础太差。正是基于这点,叮当才决定依据网上已有的一些教材为基础,从自身的体会与理解出发,编了这册不成样子的《新巫师入门手册》。但是上网后,想不到竟会收到了很多新巫师朋友的感谢、赞扬与鼓励。他们对手册的肯定,也增强了叮当的信心。于是决定在加上一篇补遗篇,补充说明LPC编程中的一些基本概念,完成这册入门教材。并斗胆考虑起中级教材的布局。
  同时,叮当也声明,所有的概念都是从我自己的理解出发,请勿与专业教材中的定义相提并论,若有贻笑大方之处,还望各路高手多多指点。

第 五 章

补 遗 篇 

第一节:变量

  首先,我发现新巫师们编程结束后,一旦update就呼啦啦地出现一大群的编译错误,其90%以上都是一些逗号,分号,括弧的基本错误。到底这些符号应该怎样使用呢?它们之间有何规律呢?但是在解释它们之前,我们必须来理解LPC中的变量与变量类型。
  变量是什么?我觉得你应该把它理解为一种不确定的替代值,有点象现实中的经纪人。其代表的人只要在第一次出来一下:声明某某是我的经纪人后,就可完全由变量来处理了。变量还有局部变量与全局之分,也就是仅仅在一个函数中起作用与在整个系统中起作用的分别。这点还是很好理解的。因此,对于我们来说,编程中之所以用到变量,其目的就是要让程序处理更快、更有效率。举例象这样一段程序:
  if(this_player()->query("qi")<25)
    this_player()->add(qi,-this_player("qi")/5);
  else if(this_player()->query("qi")>100)
    this_player()->add(qi,-this_player("qi")/2);
  else
    this_player()->add(qi,-this_player("qi")/3); 
  这段程式中反复调用this_player()->query("qi")这个值,每出现一次,程序就要找一次this_player(),将它调出来,再从他的身上取出query("qi")这个值进行处理。而使用了变量则会简化了许多。比如,象this_player(),我就定义一个me来代替它,这样,我只要在一开始声明一下,me就是this_player(),这个变量就将this_player()找出,并定义在自己身上,以后每次执行时直接使用me就行了,也就是无须再次调用。其次,我们发现this_player()->query("qi")调用也很频繁,我们可以再定义一个变量i,用它来代替它。这样,这段程式可以改写成下面这样:
  object me = this_player();
  int i = me->query("qi");
  if(i<25)
    me->add("qi",-i/5);
  else if(i>100)
    me->add("qi",-i/2);
  else
    me->add("qi".-i/3); 
  发现了吗,两个变量只是在开头定义时分别调用了一次,然后需对这两个变量进行操作便可以了。
  接着,细心的你可能会发现,这两个变量,我在定义的时候是用不同的方式的定义的。一个是object,另一个是int。这是因为我想让它们代表的类型不同。总体来说,在LPC里,变量大约有以下几种:
  object(对象型)、int(整数数值型)、float(浮点数值型即含小数点的数值)、string(字符串型)、mapping(映射型)、array(数组型)、mixed(混合型)、以及不常用的class(自定义型)。等等。

  一、object的意思,是定义一个对象,具体说来一个NPC、一个物品、一个场景、甚至一个运行于内存里的文件。它实际上是一段由后面很多变量按一定运算方式组合在一起的程式。我们经常使用的是将this_object()与this_player()通过object定义成简直的me或ob这样的符号。如果你要想在一个程序里制造成一件新的物品,则必须先定义一个变量,如:object obj;然后再obj = new(******)将这个obj实际上就clone了出来,括弧里的*****代表它的文件绝对路径名。

  二、int的意思,表明定义的变量是一个整数数字,可以为正负或0,定义出来的数字可以进行各种数字运算,但结果只保留小数点前的数字。比如:
  int i;
  i = this_player()->query_skill("force",1)/70;
  如果一个玩家的force最高只能到500级,那么这个i的结果只能是从0到7之间的这7个数之一。

  三、float相对于int来说可以是有小数的数字。比如i=10/3;如果前面是int i的话,i=3;而如果是float i的话,i=3.3333333。我查了一下外部函数表,对于我们使用的MUDOS来说,大部分的机器支持浮点值变量小数点后7位的精确度;

  四、string是说是一个字符串,你可以很简单地把它理解为一串字符号,这些字符不具有任何计算意义。一般来说,字符串的长度在理论上是没有限制的,在LPMUD里,限于网络响应,一般是在编译MUDOS时,在config.h文件里进行设置与限制的。对于字符串型变量的识别,我们有一个很简单的区别标准,就是要看它们有没有用双引号括起来,有则是string的变量,没有则看其是否整数而分辨为整数数值与浮点数值。因此在一些不严谨的语句中,如没有强制定义,也可将int、float与string区分出来。
    A、set("number",783);------->int型
    B、set("number",78.3);------>float型
    C、set("number","783");----->string型
    D、set("number","78.3");---->string型
  string型变量可以相加,但决非数字意义上的运算,而是一种合并,例如上面的C+D就是"78378.3";

  五、映射型变量是LPC独有的一种函数类型,据我的理解,好象是为了让程序更方便地实现一些小的数据库的功能。映射型变量里面有很多的小项,每一个小项都有与自己一一对应的参数。它们就好象是一个个独立的小变量一样,并使用 : 符号进行赋值。而且里面的这些小变量可以用前面的多种类型混用。 举例如下:
  mapping fam = (["a":2,"b":13,"c":2.333,"d":"一条小河","e":"158"]);
  这个fam里的a、b子变量是int型的,c是float型的,d、e是string型的。有一些LPC的说明文件里,a、b、c、d被叫做“关键字”,而:后面的象2、13、2.333、一条小河、158被叫做“内容值”。是不是有点类似于数据库的味道?映射型的变量可以用“变量名["关键字"]”的形式进行调用,并可以用“变量名["关键字"]=新内容值”的方式进行赋值。例如:
  fam["e"]的值就是"158" ,如果fam["e"]="400",那么再次调用时:fam["e"]的值就是"400"了。

  六、数组型变量实际上是很多的单个变量的集合,它的特征就是在定义变量名的时候,前面加一个*符号,前面可以object、可以int、也可以string,典型的数组型变量如下两种:
  string *num = ({"a","b","c","d","e"......});
  int *num = ({5,3,4,4,8......});
  object *obj = ({ob1,ob2,ob3,ob4});
  相同数型的不同数组型变量之间可以进行加减,加法时,则把两个数组里的项目合在一起,但是并不管里面有没有重复,一律列入。而减法则把被减变量里含有减变量里的项目统统去掉,举例说明:
  string *msg1 =({"a","b","d","d","e"});
  string *msg2 =({"b","b","d","f","g"});
  string *msg3 = msg1+msg2;
  string *msg4 = msg3-msg2;

  那么msg3 = ({"a","b","b","c","d","d","d","e","f","g"});
  而 msg4 = ({"a","c","e"});

  七、混合型变量一般用在一些特殊的地方,因为不能确定变量的类型,或者几个类型都有可能,就会用到它。不过一般的情况下,如果能确定的话还是要固定好。

  八、自定义型变量。(略。呵呵,因为我也不大掌握,基本上没用过。)

  另外象function (函数指针)用到的地方比较少,就不在入门手册中介绍了。还有一些可加在这些变量定义前面的进一步修饰的类型参数,比如象private、nomask这样的也不一定是新巫师所必须掌握的,还是留待更深一层的教材去讲述吧。


第二节 函数

  在LPC中,每一个函数被调用后,有时不需要返回任何值,有时则需要。我们就把不需要返回值的函数称为void(无返回值)型,其它的,则按照返回值的变量类型,区分为与此相互对应的类型。所以,参照上一节,我们就可以很容易地理解:函数也有着象那基本的八个变量、再加一个无返回的void,分为共九个基本类型。它们在函数开头的定义时就要写清楚了。
  所以新巫师们看到了这里后,就要使劲地想想,是否自己曾在某一个程序里,开头定义的是int ask_money(),结果在函数里面却是return "客官到底想要些什么?"这样返回是字符串的情况?反正我初写程序时常发生这样的错误。我记得在某些比较老的单机版的MUDOS里,对于函数的返回值检查并不是十分地严格,因此,在单机上测试往往很正常。但是到了LINUX下,尤其是新版本的MUDOS,对于这些检查十分地严谨,甚至在特殊的地方,还会导致宕机。
  前面我们讲过,LPC里,一个object就是一个很多变量的集合,那么这么多的变量是谁来控制它们呢,那就是函数了。在具体的编程中,每一个函数的设置都是要有其实际意义的,也就是说,要在运行中能被其它函数或其它object调用到。如果一个永远调用不到的函数,那就是没有任何意义的。在LPC中,有一些基本的函数是由系统,也就是底层的MUDOS自动调用的,我们也就无需去寻找它们的出处的。
  void create()
  前面也讲过,这是当一个object被载入内存时,对这个object进行最基本的初始状态设置用的函数。
  void init()
  当这个object本身进入一个新的object、或者有一个新的object进入了它所处的object、或者进入它自身里时这三种情况下将自动呼叫这一函数。
  然后还有一大堆由系统文件与总的继承文件所定义呼叫的大量函数,这些必须要了解,但是可以留待在实践中慢慢熟悉写与了解。
  再下来就是各个文件里自定义的函数了。其实所谓的自定义函数也只是相对的,最终说来,都是一个作者写的。只不过很多函数是由最早的巫师编写,并得到公认或约定俗成固定了下来。那么如何写一个函数呢?
  一、首先确定函数返回数据类型,比如是stirng还是int之类的;
  二、确定一个函数名,这个名字一般来说,首先你要熟悉你所工作的MUD里的函数命名规则或惯例,一是不要取一些与基本底层函数相同的名。比如die()、init()等等,其二是力求用简洁的英文或拼命取名,让人能够不看内容猜得出其用意;
  三、接下来就是一个()、()里放着这个函数执行时所需要的参数,这些参数可不是随便加的,它们的定义实际上是由调用这个函数的那段程序所提供的。
  四、写函数内容以一个{ 表示开始,最后当然是以一下 } 表示结束。函数的各种括号十分有意思,它们总是一对一对地出现。只要少了一个或多了一个,程序当然就会出错。
  五、函数一开始必须要对它所使用的变量进行声明,比如:
  string m,n;
  object ob1,ob2;
  这两句表示,在这个函数将要使用到两个分做m和n的字符串型变量与两个分别叫做ob1与ob2的对象型变量;
  六、下面就开始对变量进行赋值,计算指令的各种语句、表达式,也就是我们所看到的if、else、switch等等的语句。当然,就象别的函数调用你一样,你在这个函数里也可以调用别的函数。
  七、到了最后,再回到头来看看这个函数到底是什么类型的,只要不是 void,在最后结束的 } 前肯定要有一个 return ,并且返回和这个函数的数据类型一致的一个值。

  这里插一个与前面有关的话题,就是函数中所用到的变量问题。函数中的变量来自四个地方,第一个,当然是在函数一开始时声明并在之后直行赋值的;第二个就是在上面所说的第三步里在函数命名后面的()里面的,它是来自于调用这个函数的别的函数所提供的;第三个是来自于这个object()里的全局变量。一般是在整个文件扔程序开头的地方进行总的声明。我称它为小全局变量。这个变量可以在这个文件里所有的函数里进行调用;第四个是来自与整个MUDLIB所提供的全局变量。象我们的LPCMUD里经常会出现一些大写字母的变量名,比如象“USER_OB”“LOG_FILE”等等的变量名,在整个文件里甚至继承文件里也找不到,它一般是定义在/include目录下的全局变量声明文件里的。

第三节 符号

  编程要用到很多的符号。下面就要回到这一章开头讲的,到底那么多的符号怎么区别它们的用法。
  据我的体会,主要我们频繁使用的符号可以分出包括型与间隔型。
  
  包括型就是各种各样的括号。一共有四种,即()、{}、[]、"" 。这些括号可以掺在一起使用,但是一定要记住,在一个语句中,有几个(就必写会有几个)、同理,有几个[就必写会有几个]。所以在复杂的语句中,最好在检查时仔细数一数括号是否是前后对应的。
  一、回过头去看看第二章,就可以看到,()实质大多数是放函数执行时的参数或者是执行运算语句的。一个()前面必定会有一个函数名或者执行语词,当然有很大一部是由MUDOS提供的外部函数。比如象:write()、set()、init()或者是if()、else()、switch()等等。
  二、{}有三种用法,第一是用在函数的一开头与结尾,相互呼应。第二是用在一个程序表达式的开头与结尾。比如if(...){};第三便是被()包起来,表示数组,也就是({})。中间可以放入若干个项目;
  三、而[]也有三种用法,第一是被()包起来,表示映射函数。也就是([])。第二种是用函数名[关键字]这样的形式来表示映射里的某一关键字的值,比较常见的有在房间文件里的exits["south"];第三种是直接在一些string型或int型的变量后面跟上一个[],里面有一些参数,根据具体定义的返加值类型,返回不同的值。比如:
  string msg = "tims";
  (string)msg[0]就是t、(string)msg[3]就是s。
  而(int)msg[0]则会返回一组数字。具体数字的含义我也不太清楚,不过据我反复试验,发现这些数字的高低可以判断这个msg是英文字母、英文字符、中文字符或是全角字符。好象是各个字符的区域代码一样。
  四、""用在两个地方。一:在函数的具体项目名上要加。比如set("age",14);当然,如果这一个项目是一个变量或已经被一个变量所代替了,则不能加。二、在字符串上必须要加,尤其是表示字符串意义的数字。否则若没有定义的话,很容易被当作int型处理。而只要加了"",则必定被当作字符处理。

  间隔型符号主要只有两种:,与;与:
  一、逗号:,  逗号一般是表示前后的项目是平等并列的。它常被用在数组的各个数之间的分隔、映射中各个不同关键字的分隔,如:
  string *str = ({"A","B","C","D"})
  或者再如:
   mapping quest = (["A":4,"B":"大河","C":"15","D":31])。
  在一个函数的一变量声明中,它用于分隔同类不同名的变量名,在函数命名后()里的参数也是逗号相隔。当然这里有一处例外,就是在一些mapping型函数里,如果是采用set的方式,在总的映射名与后面的各项关键字之间也是用的是逗号分隔的,比较常用到的如:
  set("exits",([......])); 
  二、分号:;  分号表示一个完整的语义讲完了、执行完毕。每一个分号前的话都有一定的独立的意思。因此,在某一个独立的变量内部是绝对不会出现分号的。
  三、冒号::,冒号一般用在三个地方,一是单独使用时,常常用在映射(mapping)里,表示将冒号右边的值赋给左边。左边的叫关键字,右边的叫做内容值。 二是与?合用,例如:
  A?b:c
  在这里,A是一个条件表达式,如果A成立的话、或者是真的话,就会返回冒号左边的b值,如果不成立,则返回冒号右边的c值。这种写法用在一些简单的判断里,可以省去很长的if else。
  第三种情况是在swtich()语句里,放在case <某一项>的后面,表示,如果swtich()里的可能是这某一项时的情况。例:
  swtich(random(10))
  {
    case 1:
  ....... ......

  最后再说一下,在程序中,象if() else() switch() 这样的判断语句后面直接跟着{},不需要加间隔符号。而且如果{}里面的内容只有一行的话,这对{}可以省略。例:
  if(me->query("age")>45)
  {
    write("it is good!\n");
  }
  就可以写成:
  if(me->query("age")>45)
    write("it is good!\n");
  
  再下来就是一些逻辑符号了,象&&表示并且、||表示或者、=表示赋值。
  运算符号,+-*/也就是我们四则运算了。

附录:常见编译出错信息

均以/u/llm/npc/test.c文件为例:
一、编译时段错误:/u/llm/npc/test.c line 13: parse error
  parse error一般表示错误出在基本的拼写上,多是象逗号、分号写错,或者是各种括号前后多写或漏写的情况,可以在提示的第13行或之前的几句里找一找;
二、编译时段错误:/u/llm/npc/test.c line 13: Undefined variable 'HIY'
  Undefined variable表示有一些未曾定义、不知其意义的东西存在。后面跟着的就是这个不明意义的字串。象这句就表示不知道第13行中的'HIY'是何意思。这个错误有三种可能,一是将一些变量的词拼错。比如,本来定义的是"HIT",结果写成"HIY"。二是因为这个变量未曾定义或者根本就没有声明,第三种情况是这个变量是定义在一些继承文件里,但在这个文件里却忘了继承。象这行就是最后一种情况,是这个文件前没有#include <ansi.h>,因为表示亮黄色的HIY是在/include/ahsi.h文件里定义的。
三、重新编译 /u/llm/npc/test.c:错误讯息被拦截: 执行时段错误:*Bad argument 1 to call_other() 
  这句在开头,一般是指这个文件里在调用其它文件里的函数或者是对象时发生错误了。这时你可以接着往下看。一些与其它文件相关的错误信息全部跳过去,直接找有关这个 test.c文件相关的错误信息,然后找到比如象这样的信息:
  程式:/u/llm/npc/test.c 第 47 行 。那么就仔细查看第47行调用的东西有无问题。
四、重新编译 /u/llm/npc/test.c:错误讯息被拦截: 执行时段错误:F_SKILL: No such skill (froce) 
  这个错误很明显的,肯定是在设置武功时把force写成了froce,系统当然找不到froce这样的skill了。
五、重新编译 /u/llm/npc/test.c:编译时段错误:/u/llm/npc/test.c line 75: Type of returned value doesn't match function return type ( int vs string ). 
  这句表示在某一个函数里,返回值的类型与定义的不同,并指出是因为string与int的错误,到75行附近检查吧。
六、重新编译 /u/llm/npc/test.c:编译时段错误:/u/llm/npc/test.c line 72: Warning: Return type doesn't match prototype ( void vs int ) 
  这句也表示错在函数类型上了,只不过是因为函数与前面的声明相冲突,一个是int,一个是void。
七、重新编译 /u/llm/npc/test.c:编译时段错误: /u/llm/npc/test.c line 5: Cannot #include ansii.h
  很明显,在第5行处想要继承的文件并不存在。是不是自己写错了?
  
  后记:写完这篇《补遗篇》,这册《新巫师入门手册》就算结束了吧。相信你将这五章都真正看懂,并理解了之后,做一个日常维护的巫师也就可以了,而对于写一些简单的场景、NPC更不在话下了。有什么意见与想法将点击左下角的巫师信箱,给我来信。我们在以后的有关中级教材里再见面吧!
                  叮当(2000年7月)


二 系统刷新与内存清除分析 

  有关系统更新一直是玩家乃至于新巫师们关心的问题。比如,为何每隔15分钟大多数房间里杀死的NPC会重生?跑到别处或被玩家背到别处的NPC怎么会跑回去?为什么有的NPC跑不回去?什么有的东西会重生?为什么又有的东西只要别的玩家放在身上?等等。
  目前主流MUDLIB都是ES系列的。从ES系列沿袭下来的更新都是通过ROOM的更新实现的。而ROOM的更新则是由MUDOS里的设置每隔一定时间(一般是15分钟)调用一次所有的有reset()函数的房间。而这个reset()函数则写在ROOM的标准继承文件里面。下面我们则来看看ROOM是如何实现房间里的生物、物品的重生或更新:
  在写这篇文章之前,正好在网上看到darks兄写的《ROOM的结构》,于是我这篇文章的不少地方也就写得很顺畅了,有些直接引用了《ROOM》一文的一些内容。为了尊重原作者,凡是引用或出自darks兄的原文内容我都用“”与绿色标出:
  ROOM的标准文件由于MUDLIB的不同,放在目录路径也不同,但大多情况下也就是/inherit/room/下或者与/obj/room/下两种可能而已。反正不检查一下在/include/下的globals.h,看这个文件里ROOM是定义在哪里就可以了,下面来看一看room.c的程序详解:

inherit F_DBASE;
//“这个是继承dbase标准继承,有了它,你才可使用set等函数为这个物件设定变数”(此问题日后做专题说明)。

inherit F_CLEAN_UP;
//“这个用来定时清除很久没被访问的room”,这个概念我们要在后面谈到。

static mapping doors;
//“这是一个有关房间里的门的全局变量,不是我们今天讨论的范围之内,你只要知道就行,我们在这个文件里还能找到与门相关的几个函数:”

mixed set_door(string dir, string prop, mixed data)
mixed query_door(string dir, string prop)
mapping query_doors()
string look_door(string dir)
varargs int open_door(string dir, int from_other_side)
varargs int close_door(string dir, int from_other_side)
varargs int lock_door(string dir, string key, int from_other_side)
varargs int unlock_door(string dir, string key, int from_other_side)
int check_door(string dir, mapping door)
varargs void create_door(string dir, mixed data, string other_side_dir, int status)
int valid_leave(object me, string dir)
int query_max_encumbrance() { return 100000000000; } 
//设置可容纳的重量,以上这些函数大多与门有关,我们今天都一一略过,下面才是我们今天要研究的与系统房间刷新相关的函数:

object make_inventory(string file)
{
  object ob;
  ob = new(file);
//根据传递来的路径名,将ob复制出来 
  ob->move(this_object());
//复制出来的ob移于目的地 
  ob->set("startroom", base_name(this_object()));
  return ob;
}
//这个函数用来产生一个房间里的物品。首先它需要别的函数在调用它的时候要传递给它一个需要产生的物件的路径。然后用new()复制出来,接着move到这个房间里,再接着给它设上startroom这个标记,这个标记就可以在这个房间定时呼叫自己房间里产生的npc可以使用return_home()这个函数时,正确回到原来的地方。

void reset()
{
  mapping ob_list, ob;
  string *list;
  int i,j;

  set("no_clean_up", 0);
//“这个标记为零,即允许系统到了规定时间将这个文件扫出内存,那么这个文件内的所有东西都会消失。由于room标准继承有这句,似乎发现只要继承它的房间文件无论写为0/1都是无效的,因为都会在这里被清除成零。”

  ob_list = query("objects");
//先取出一个这个房间初始设定的objects的映射集
  if( !mapp(ob_list) ) return;
//如果这个房间初始时就没有设定有生物物品,就说明根本无需要刷新,因此到此返回。

  if( !mapp(ob = query_temp("objects")) )
  ob = allocate_mapping(sizeof(ob_list));
//程序到后面才可看到ob = query_temp("objects")是如何出来的,在这里,我们先不管,你只要知道,如果是一个刚刚编译进内存的房间,是不会有ob这个映射集的,因此需要用allocate_mapping按照ob_list的多少为这个新设定的映射集ob分配内存大小。

  list = keys(ob_list);
//从ob_list映射中取出关键字组成一个新数组。

  for(i=0; i<sizeof(list); i++)
//开始循环检查这个数组里的每一项 
  {
    if( undefinedp(ob[list[i]])
      && intp(ob_list[list[i]])
      && ob_list[list[i]] > 1 )
      ob[list[i]] = allocate(ob_list[list[i]]);
//如果房间里曾经定义了要产生物品,并且数量不止一个的话,就要进行ob[list[i]]这个物件数组的内存分配

    switch(ob_list[list[i]])
    {
    case 1:
//举例一个文件里:set("objects",(["/d/city/npc/bing":1]));,那么在这里,也就是ob_list[list[i]]这个值取出是1

      if( !ob[list[i]] )
        ob[list[i]] = make_inventory(list[i]);
//如果这一个对象已经不在了(玩家理解的就是被杀死了或被当作任务送掉了,巫师的理解就是被destruct了),就使用make_inventory()函数再重新制造一个放进来。这里注意了,仁去递过去的list[i]就是这一项物品的路径名,正因为有了路径名,make_inventory()函数才能正确制造出新的来。

      if( environment(ob[list[i]]) != this_object())
//反之如果还存在,但它目前所处之地却不是目前的这个房间

      {
        if(ob[list[i]]->is_character()
          &&!ob[list[i]]->return_home(this_object()))
        add("no_clean_up",1);
//这句判断该物体如果是生物,就呼叫生物的return_home()叫它回来,如果这个NPC不能回来并且返回值是0的话,就会给这个房间增加一次no_clean_up的记号,程序的原作者之所以要在这里增加房间的no_clean_up记号,估计它的意思就是不想让系统在房间不能成功召回自己的NPC的情况下清除它,因为它想在以后的刷新中再把它呼叫回来。但是实际上,大家注意到前面的程序了吧,只要产生了下一次呼叫reset()时,前面就会把no_clean_up设为0,因此这段ES的源程有些莫名其妙,但大家居然都没人改,也是怪事。

      }
      break;
      default:
//除此之外,也就是物件不止一个的话,举例相当于文件里:set("objects",(["/d/city/npc/bing":2]))或者3,4....这类的情况

      for(j=0; j<ob_list[list[i]]; j++)
      {
        if( !objectp(ob[list[i]][j]) )
        {
          ob[list[i]][j] = make_inventory(list[i]);
          continue;
        }
        if( environment(ob[list[i]][j]) != this_object())
        {
          if(ob[list[i]][j]->is_character()
          &&!ob[list[i]][j]->return_home(this_object()) )
          add("no_clean_up", 1);
        }
      }
//这里其实与物件只有一个是一样的,只是因为相同的物品不止一个,需要进行几次的循环判断而已。

     }
  }
  set_temp("objects", ob);
//看到这里,知道这个函数里ob映射集是如何来的了吧,实际上ob_list就是代表的这个房间里的的query("objects"),是一个字符串内容的映射集,而ob就是代表的这个房间里的query_temp("objects")它实际上一个object型的映射集。
}

  reset()函数结束了,其实在ROOM里,除了这两个函数,还有一个在一开始编译进内存后进行首次调用reset()函数的setup()函数之外,其它的函数都是有关门的,都是可以去掉并影响房间的主要功能的,ROOM标准继承的最主要功能就是定时检查自己房间里的物品是否还在?是否需要更新等等。而这个定时则就是由MUDOS定义并按时呼叫房间里的reset(),这个时间绝大多数被定义为十五分钟。
  我们通过上面的程序详解可以看出,当一个房间被编译成功进入内存之后,那么这个房间就将自身产生出来的各个物体(假如它有的话)记入一个query_temp("objects")的物件映射变量中,这个变量与我们写程序里的query("objects")是一一对应的,只不过query("objects")里记的是这此物件的
文件路径,而query_temp("objects")里记的是这些具体的物件。关于这两个映射的区别,有兴趣的新巫师可以找一个有很多NPC的房间按下面分别call两次,看看区别:
call here->query("objects")
call here->query_temp("objects")

  在reset()被调用时,程序就会循环地一个个地查找这些物件是否还在MUD中?如果这些物件都已经不存在了,那么,reset()函数就会通过呼叫make_inventory()函数将其再次制造出来,也就是我们看到了,更新时间一到,很多被杀死的NPC,用掉的东西都会在原处产生出来。
  而如果这些物件都还在MUD中,就会检查它们是否还在原处?如果不在的话,只要是生物,就呼叫它的return_home()函数(这个函数在所有NPC的标准继承
/inherit/char/npc.c里),叫它回来。并且要把这个房间作为参数传递过去,否则NPC会回不来。如果不是生物只得作罢(这就是房间产生出的物品如果被某一玩家放在身上,就再也不能重生的原因)。那么下面我们就来看一下npc.c里的return_home()函数:

int return_home(object home)
//注意,括号里的home就是呼叫它回家的那个房间,当时是叫this_object()
{
  if( !environment()|| environment()==home ) return 1;
//再次检查:是否在一个存在的环境里?是否已经回来了?如果是,则什么也不做,返回!
  if( !living(this_object())|| is_fighting()) return 0;
//如果NPC处于昏迷或战斗状态,则不回来,返回值是0,综合room.c,原房间会增加no_clean_up记号;
  message("vision", this_object()->name() + "急急忙忙地离开了。\n",environment(), this_object());
  return move(home);
}

  谈到这里,大家可以发现,所谓房间的更新,实际上只是房间里的物体进行更新,这个房间没有任的变化。也就是说,如果在房间更新的时候,我们站在这个房间里,或者我们扔了一个不属于任何房的物品在这个房间里,都不会受到影响,这些物品与我们在更新前后都不会消失。这个与我们巫师进update here是本质性的两回事(updata here就是更新了房间)。

  那么,有时有的玩家就会说,我曾得到一个很好的宝物,离线不能保存,我就把它扔在一个很少有去的地方,结果,每次再去连线再去找的时候,大多数时候都找不到,不会是被别人捡去吧?这里就及到另一个概念:MUD里的资源清除。
 
  大家知道,在LPMUD里,所有的程序都必须装载进内存里才会工作。因此,MUD的内存资源便就是最主要的资源。更合理地分配和使用内存便成为一个MUD效率高低的体现。
  MUDOS为了节约内存的耗用,对于每一个占用内存的对象,包括是房间、物品、人物、指令等等,如果相当长的时间内没有被其它程序参考到(参考的含义:就是包括别人进入、看到、或者使用到这个房间、物品、或指令,还包括各个程序等等)的话,也就是这个对象很长时间没有活动了,MUDOS就会调用这个对象的clean_up()函数(由于大多数的程序都会继承这个函数标准文件),如果该函数返回1,则下次同样情况还会调用该对象的clean_up;如果返回0,则永远不再调用。那么,我们就来看一下/feature/下面的clean_up.c文件,这个文件只有一个函数:

int clean_up()
{
  object *inv;
  int i;

  if( !clonep() && this_object()->query("no_clean_up") )
    return 1;
//如果这个对象不是clone出来并且有"no_clean_up"记号的,则返回1(返回1的含义上面说过了)

  if(interactive(this_object())) return 1;
//如果对象是互动物件,比如玩家,就返回1

  if(environment()) return 1;
//如果对象处在一个环境里,也返回1

  inv = all_inventory();
//取出这个对象里面所有的物件
  for(i=sizeof(inv)-1; i>=0; i--)
  if(interactive(inv[i])) return 1;
//循环检查这些物件,只要其中有一个互动物件,就返回1

  destruct(this_object());
  return 0;
//全部检查完了后,就决定正式摧毁自身,释放出这个对象所占用的内存,并返回0
}