用户工具

站点工具


course:mudlib的打造

跟Akuma一起从头打造mudlib

第零讲:写在前面的话

作者:Akuma

转自mudbuilder.com,akuma原创,akuma是北大侠客行老牌巫师柳残阳

经常在这里遇到有朋友就一些特定的mudlib提问,很多人,特别是初学者,经常忘了写是哪个lib里的问题。

一般来说,目前我们常见的就是xkx类,fy类,xyj类的lib。

虽然这些lib最初都源自台湾安老大的ES2 mudlib,但经过这么多年的发展或者不发展,各自都有很多自己的特色(不是指游戏内容,而是目录结构,继承关系,乃至于函数命名风格等)。有时候我自己不去对应的lib里翻找,也很难记清楚一个事情的来龙去脉。更不用说众多的改版站自己做的东西了。

而且由于这么多年的变迁,各种lib都已经变得相当的复杂,很多基础的东西隐藏的比较深。包括很多还在写东西的wiz,自己对于一些基本的因果关系也不是那么熟悉和清楚了。

所以我就在想,是不是可以采用类似写编程书的方法,通过例子一步一步的把一些底层的东西讲清楚说明白。对于大家来说,越是对底层了解的透彻(知其然并且知其所以然),在搭上层的时候就越容易采取简单而且正确的方法;对我来说,也可以借这个机会整理我自己的思路,并且把一些我自己也比较迷糊的概念整理明白。

所以才会考虑写这篇东西。

我希望可以通过从头整理一套简单清晰的lib,把很多底层的概念配合者讲解一遍。

那么有四点需要说明:

  1. 这是比较业余的东西,我自己工作比较忙,可能很难保证更新的速度,但我一定尽我所能来写,争取把这个系列完成掉。
  2. 由于演示的成分比较大,因此随着每一讲的深入,我们最终完成的lib可能会是个简陋的、无法真正提供游戏内容的产品。但我希望他是一个简单的,思路清晰的东西,可以完成大部分基本的游戏内容(比如可以提供简单的战斗,经验的获得等)
  3. 如果可能的话,我会在讲到每一点的时候,对照一些成熟的lib来讲。但是我的目录结构,乃至于继承关系等可能跟他们有不同。
  4. 依然由于这是个人作品,我自己的思路也会比较固定,所以错误在所难免,如果大家发现描述上的、概念上的错误,那纯熟正常,请大家尽可能提出来,我尽量fix。

第一讲:让它跑起来

注:每一讲我都会上传一个相符的lib,有些文件是旧的,有些是新的,我尽可能在lib里写清楚注释。更详细的内容则在每讲的正文里写。

一个最简单的能跑的lib应该长成什么样子?每个基于mudos写lpc的人可能都会给出不同的答案。我记得曾经有个朋友释出过一个不到5k的lib。

我这个则还要小一点,tgz之后是1851个字节。嗯。。。还好。

我们对这个lib基本上不会有什么期待,但是他至少应该完成如下两个事情:

  1. 能跑起来,并且接受用户的连接(你用zmud也好,telnet也好,总之是可以连到端口上)
  2. 连接后的用户可以输入,并且lib应该给予一定的反应(那么最简单的做法就是完成一个所谓的echo server了—你输入什么,server就给你返回什么)。

【配合本讲的lib版本为0.1,文件名则是newlib.0.1.tar.gz,见附件】

以下是目录结构:

.
|-- adm
|  `-- obj
|      |-- master.c
|      `-- simul_efun.c
|-- include
|  `-- globals.h
|-- log
`-- obj
    `-- user.c

首先说目录结构,一个好的清晰的目录结构用起来很舒服,我自己一般推崇单层目录结构,但是考虑到习惯问题,我在/adm下采用了和xkx类似的方法,即把master和simul放到了/adm/obj/下。

大致讲一下:

目前我们只创建了四个基本目录:/adm /include /log /obj。

  • adm放的都是“独一无二”的东西,比如最关键的两个物件master.c和simul_efun.c,还有以后会慢慢出现的各种daemons。
  • include是所有头文件所在的目录,目前只有一个globals.h。
  • log是用来存放各种log输出的,这没啥可讲的。
  • obj是所有lib当中最终被用到的物件的实体文件,也就是会被最终new()或者load_object()出来的东西。暂时只有一个连线物件,也就是user.c。(你看,现在我们连login.c都省掉了,因为我们的当前目标仅仅是能让mud接受连接而已,断线重连、帐号认证等工作要放到以后完成)

这里我们先讲一点基础的东西:mudos如何启动?

我们不讲mudos自己,只说和lib有关的部分。

有人认为,mudos最先载入的是master.c(请注意,这个master.c不是那个可以用来拜师的master),其实这个看法是有点问题的。

一般来说,mudos最先载入的文件是simul_efun.c。

我们知道,要想跑起一个mud,除了lib以外,还需要driver和一个给driver用的配置文件config.cfg。在config.cfg里,有三个文件是要被定义的:simul_efun.c,master.c和globals.h。如果愿意的话,我们当然可以给这三个文件起个其他的名字,比如master.c改名叫core.c,globals.h可以叫做dangzhongyang.h之类的,随意。。。不过大家都习惯了,我就不改了。


重点1.关于simul_efun.c

刚才我们说到,mudos启动之后,会先尝试载入这个simul_efun.c。

这里有一个概念就是所谓的simul。众所周知,mudos为了游戏的目的,提供了大量的函数(efun)来帮助我们完成工作,比如说像判断是否为玩家的userp(),像是对字符串做操作的一系列函数等等。

然而mudos也不是万能的,他不可能为预先想到所有可能的需求,有时候我们必须自己动手封装一些功能(比如说,log_file()这么方便而好用的功能,mudos就没有提供),如果应用范围很窄,我们当然可以直接把他写到代码里,但是像log_file这么大众化而且常用的功能,我们不可能每次用到就c/p到对应的文件里去,一个是不方便,一个也容易出错不是?

于是,mudos提供了一个方法来解决这个问题,这就是simul efun,简单说就是“模拟的efun”,这个功能可以帮助我们封装合心意的函数,并且在lib的任何地方像使用efun一样的使用他(虽然说慢点吧。。。。当然从效率上说,最快的办法是把efun移植到mudos里,真正做成一个efun,然而这是另外一个话题,不是我们讨论的范畴)。

完成这个工作所需要干的事儿不多,也很简单,就是把函数定义和体写到config.cfg指定的simul_efun.c当中就好了。(具体请参考我的lib当中simul_efun.c里的log_file()函数),这样我们就能让他像个efun一样的工作了。

题外话1

很多mudlib经过了很长时间的发展之后,积累了大量的simul,统统塞到simul_efun.c当中也很困扰,不美观也不便于管理,因此他们会采用另外一个做法:继承。

简单说就是把simul_efun.c当作一个入口文件,把所有的simul函数分门别类的写到其他文件当中去,在主文件里只用继承(inherit)的方法来把这些“其他文件”包含进来。(用include貌似也可吧。。。我不是很确定)


题外话2 关于lpc的OOP特性

lpc这门语言出现的时间较早,虽然一般来说我们认为他是面向对象的,但是也并不完全符合面向对象的所有观念。

比方说,他只支持对对象的函数引用,而不支持对对象的元素引用,当我们想获取或者改变一个对象中的变量时,我们只能通过函数来实现;

再比方说,对于封装来讲,LPC并不能完好的实现多态,并且只有限的支持变长参数表(varargs,lpc只支持一个变长参数)。

题外话2纯熟废话,有兴趣的同学可以自己参考OOP


重点2.master.c当中的connect()函数

我们本讲开篇就说了,当下最重要的是让mudos能够接受连接。所以这里我们有必要讲讲mudos接受用户连接的机制。

我们知道,LPC最有趣的地方就在于他是OOP的(不完全无所谓,反正了解到lib里所有的东西都是对象就ok了),那么给每个用户连线分配一个object是很正常也是很方便的管理方法。 问题来了,如何让mudos知道“这是一个连线物件”呢?

这里就用到了一个很重要的master_apply(这玩意儿我们下边讲到),master_apply::connect()

他的原型是:

object connect()

这个函数的作用就在于,当mud接受到一个连接之后(不管你是用zmud还是telnet,总之你连上了mudos提供的端口服务),mudos会调用master的connect()函数,并且期望返回一个对象。

这个对象就相当于在mudos里挂了号了。mudos会把它当作一个用户连线对象来对待,比如说会认为他是userp()和interactive()之类的(这几个函数有一些细微而且诡异的差别,有空我们再说)。

请看我的lib里的connect():

object connect()
{
        log_file("new_user_login",time()+"\n");
        return new("/obj/user.c");
}

很简单,master.c只要被动的等待mudos的呼叫,并且在合适的时候造一个user_ob就ok了。

这里多讲一句:

一个完整的登录过程,在mudos当中包括两步:

  1. 调用master apply的connect()获取连线物件;
  2. 调用这个连线物件身上的另外一个apply函数:logon()

其中第二步是为了给登录验证过程一个合适的调用接口用的。目前我们0.1版的lib还用不到这么牛b的技术,所以他就悬空了。

未来适当的时候我们再补充上。


重点3.master的apply

实际上master.c当中除了构造函数create()之外,基本都是apply。

什么是apply呢?

大概说一下,所谓apply,就是那种由mudos隐形调用的函数接口,这些接口是为mudos提供服务的,我们通常在lib当中,只是被动的通过这个接口告诉mudos在某些情况下“可以”或者“不可以”,或者“应该是谁”这样子。

在普通对象身上的这种接口我们就把他叫做apply,在master身上的就是所谓的master apply了。

通常大家见到的比较多的apply包括像 id()(被present()隐含调用),像reset()(被reset机制调用)。

master里更多是跟权限有关的master apply,比如valid_xxx一族~~~

ok,这里我们大概知道有这么个事情就ok了,具体以后碰到需要用的apply,我们再像这次的connect()一样讲解。


重点4.globals.h

大家会不会遇到这样的问题,我定义了一个宏,却忘了他在哪个头文件里?或者是写一个.c的时候经常要包含无数的头文件,其实就为了他当中一两个宏而已?

感谢mudos的作者,他通过globals.h帮我们在一定程度上解决了这个问题:

当我们有些宏定义是非常全句化的(比如说对目录的定义,对一些重要ob的定义等等),我们可以把它们丢到globals.h里。

并且,更方便的在这里:我们不需要显性的在程序里include这个globals.h,mudos自动的帮我们在每个.c当中都包含了它。

所以,当你有一些宏很全局的时候,尽管塞给globals.h吧。。。

另外一点,请看现在这个非常简单的globals.h

#ifndef __GLOBALS_H__
#define __GLOBALS_H__

#define LOG_DIR                "/log/"

#endif

发现有什么不一样没有?

我们使用了#ifdnef #define #endif的方式。

这样做的好处是,避免出现由于.h嵌套而导致的redefine(比如说a.c包含了a.h和b.h,不幸的是a.c同时继承了b.c,而b.c自己也包含了b.h,这样就出现了实际上的嵌套)


重点5.user.c

我们的user.c现在功能很简单。在正常的lib当中,为了方便登录认证和断线重连,这个文件被分成了两个。即login.c和user.c(也许有人直接是用body.c?我不太确定,总之是这么个意思)。一个负责密码认证,一个负责真正的用户行为和数据。并且通过exec()函数把mudos认为的用户标签在两个之间切换。

现在我们这里非常简单,不认证,所以直接就只有一个user.c,未来再细化这里。

接下来,mudos如何获取用户的指令呢?

在正常的mudlib当中,我们通常通过一系列复杂的行为来实现之,例如通过一个“全时的”的add_action(command_hook)钩子来获取用户输入,并且通过commandd.c之类名字的一个daemon来找到恰当的指令文件,并执行之。

未来我们也会完善这样的一套结构,不过现在么。。。既然我们只想完成一个echo server。我们暂时用一个更简单的方式来处理他:string process_input(string arg)

这又是一个apply,在“正常的”mudlib里,我们通常用这个apply来实现global alias的解析。

这里我们就直接通过

string process_input(string arg)
{
        write(arg+"\n");
}

这样的模式来完成“echo server”的工作了。

这边有个问题,大家有没有发现一个string型的函数我没有return,这直接导致跑起来之后,我们每个指令敲进去都会返回一个>what?

不要紧,反正这个版本我们只是大概的演示,下一次我们修好他。

好了,这次一共就四个文件,我们构建了一个可以跑起来并且有反应的lib了。大家可以跑一下看看(附件里我提供了一个bin文件,包含了driver和config.cfg,是linux版的,由于我在我的服务器上跑的driver非常多,所以我给driver改名叫mud以避免不幸被killall掉。)

如果您想在linux下跑,只要chmod +x mud,并且重新配置一下config.cfg里的mudlib和bin的目录就ok了。

如果您想在win下测试,那么很抱歉,我手头没有合适的mudos.exe版本(哪位好心的老大提供一个)


小结和预告

第一次写这种东西(声明一下,我没写过语言类的书,所以不太会写,可能结构太乱,内容也很分散,抱歉),大家有啥建议和意见欢迎跟帖,我尽量改进。

附件是两个文件:bin.tar.gz 是linux版的mudos和config

newlib.0.1.tar.gz是和本讲座同步的lib。可能有个问题,就是里边涉及到的中文是utf8格式的,大家用起来可能是乱码,这个倒是可以用uedit转一下码(主要是我习惯了在shell下直接敲代码,也就不改编码了,好在作为一个讲解用的lib,不会有太多中文内容在里边)。

下一讲,我想我会把登录验证和指令系统补全。这样我们就可以有空间做一些实验了(用指令来写测试代码很方便的说)。

另外,如果可能,也许会补一个不那么完善的权限系统,也许不会,我看进度把。。。

补充一句,bin.tar.gz里用的就是标准的v22pre11的mudos,我只做了两个小改动:

  1. 是在mudos跑起来的时候会生成一个以config文件名+“.drtmp”为名的临时文件,里边存放了mudos自己的pid
  2. 是稍微修改了efun::sprintf(),让它能够支持functionp,(这是当时我为了简单化的实现背包内物品存储时打的补丁,防止带有functionp的物品存储出错)

两个都没有什么实际用途(起码对这个lib来说是如此),所以大家如果用没有修改过的pre11也没问题的。

补充一下,上文说到的诡异的三个函数是:

userp(),interactive()和living()


附件

第二讲:指令系统

我思考了一下,决定暂时跳过登录部分,先说指令。毕竟有了指令之后,就可以在线更新和重启,不用不停的kill driver进程了。

在开始之前,我们先思考一个问题,指令是干什么用的,以及我们希望如何管理指令系统。

我们常讲一个词叫I/O,也就是所谓的输入输出。

从最基本的意义上说,指令就是玩家敲的东西,他希望通过敲一些东西,告诉mud“我要做什么”。抛开网络层和mudos底层不讲,我们可以把指令看作是一个字符串(事实上他的确是一个字符串,并且是以\n或者\n\r结尾的字符串,也就是说,很不幸,我们的一telnet为基础的mud不支持指令或者指令参数里带有回车。。。)

然后,mudlib根据一定的规则,找到一段适当的程序去解释玩家的指令,并且给予适当的操作和反馈。

所以我们第一步可以把指令系统归结为如下的需求:

  1. 想办法获得玩家的输入
    1. 注意,这个规则一定要是规范的,这个规范我们可以随意制订,比如说用100到999的三位数字表示特定含义的指令;当然了,理论上这虽然没问题,但是考虑到我们的zmud或者telnet,玩家的指令是手敲的,你让他记这么多数字不太现实。
    2. 于是我还是决定继承传统mud的方法,用“指令英文+空格+参数表”的方式来设计这个规范。
  2. 我们定一个规则,让玩家输入的指令可以在mudlib里找到那段我们希望他来执行的代码去执行之。

如前所述,我们已经假定了指令规则是 “指令英文+空格+参数表”,那么指令的英文名字本身就是一个很好的代号,我们只要通过一定的编码,赋予不同代号对应的程序段就好了。


本次讲解的内容分成两个部分,第一部分是简单实现指令,第二部分里我们再进一步细化它。

第一部分代码见附件 newlib.0.2.1.tar.gz

目录结构如下:

.
|-- adm
|  |-- daemons
|  |  `-- cmd_d.c
|  `-- obj
|      |-- master.c
|      `-- simul_efun.c
|-- cmds
|  `-- usr
|      `-- test.c
|-- include
|  `-- globals.h
|-- log
`-- obj
    `-- user.c

重点1.我们首先来解决“获得玩家输入”的部分

请看这个版本的/obj/user.c。大家还记得吗?上次当我们写第一个echo server的时候,留了个尾巴。当时我们是通过process_input()来“处理”指令。

这一回,我们希望可以把指令系统建立起来,因此我把process_input()的处理暂时注释掉了。

如何获得玩家的输入?

这里我继续使用一般mudlib(es2 类)的做法,通过一个“全局的”add_action()来实现。

请看代码:

void create()
{
        setup();
}
int setup()
{
        log_file("user",sprintf("%O setup at %s\n",this_object(),ctime(time())));
        enable_commands();
        add_action("cmd_hook","",1);
        return 1;
}

正常来说,我们不应该这么早进入这个部分,不过由于暂时还没有登录认证体系,就先这么凑合。这里只是用来表述流程:

一个user_ob被master.c的connect()创建时(记得吗?这个时候他就是一个真正的连线物件了),apply函数create()被呼叫,于是他调用了setup()。

我们可以看到,setup()只干了两件事:

  1. enable_commands();
  2. add_action(“cmd_hook”,“”,1);

add_action()大家在制作谜题的时候会经常遇到,就是给某个物件增加一个“临时的”指令,并且指定用于解释这个指令的函数名字。所有“接触到”这个物体的物体,都可以使用这个指令。 我们这里的用法比较奇怪一些,他大概的意思就是“不管你输入什么,我都认为你是我这个add_action定义的指令”。

我一直觉得这个写法挺古怪的,不过为了兼容和尊重传统,我们继续这么搞吧。

enable_commands()是给物件设置一个标记,好让add_action()有效。

继续看cmd_hook()这个执行函数。

int cmd_hook(string arg)
{
        string verb = query_verb();
        return CMD_D->do_cmd(this_object(),verb,arg);
}

query_verb()这个efun是用来获取玩家的“最近一个指令”,注意是指令,不包括参数表。参数表是由add_action给定的解释函数(这里就是cmd_hook以参数形式传进来的)

然后,我们通过一个daemons来设法去执行这个指令(也就是verb)

return CMD_D->do_cmd(this_object(),verb,arg);

CMD_D一会再看。这里我们要注意的有一个要点,请看从cmd_hook被定义为int型函数开始,我们一直在return(CMD_D里也是一路有return的)。

为什么?这是mudos的一个机制,针对玩家的输入,我们一路处理下来,如果有为true的return。那么系统会认为这个指令被正确的执行了。否则他就会想办法给出报错。

从我们之前看到的“>what?”或者一般mud里定义的“>什么?”,一直到使用notify_fail来自定义的错误返回等等。


重点2.规范的管理和找到指令对应的处理函数

在前边我们已经获执行指令取到了所有必要的信息,包括指令名字,参数表(还有是“谁”的指令),接下来我们就要开始想办法执行他了。

请看/adm/daemons/cmd_d.c(有些lib里可能叫做commandd.c之类的)

目前我们只有这么一句

int do_cmd(object me,string verb,string arg)
{
        return ("/cmds/usr/"+verb)->main(me,arg);
}

这也就是刚才在cmd_hook()里调用的函数体了。

这里不用多解释,我们就是通过“/cmds/usr/”+verb拼出了需要使用的指令的文件名字,然后把“谁”和“什么”传到他的main()函数里了。

其实在lpc里,main()并不是一个特定的函数,指令里用这个函数名作为入口,我猜是当年某个习惯c的人随手定的规则。不过为了讲解的作用,我们还是继承下来。

我们特意创建了/cmds/usr/test.c这样一个指令,大家可以自己看看。

ok,现在我们的指令流程已经初步创建起来了,如果你现在启动我们0.2.1版的lib,连接上去,敲:

test xxxxx

就会获得如下的反馈:

in cmd test: arg=xxxxx

这说明指令test已经正确的被执行了。

兴奋的我们准备再敲点别的。。比如asdfasdf asdfsdf这样子。。。

嗯。。。。没有反应。

如果你这个时候去看/log,就会发现多了一个error_handler目录。很不幸,一般来说,看到这个就表示我们的lib有“runtime error”(运行时断错误,也就是说,程序可以被编译,没有语法错误,但是在运行中出了错)。

这说明我们的程序缺乏起码的容错性。

这也就是我们在 lib.0.2.2.tar.gz当中需要解决的内容。


有点小忙。。。

稍后我把第二讲的下半部分补全。需要解决三个问题:

  1. 鲁棒性,无论如何我们不希望总出现error_handler。也就是说,无论玩家怎么乱输入,我们都应该可以处理,而不是任由它报错。。。
  2. 便于管理,我们希望找到一个方法,让指令的处理文件有序的呆在一个地方,并且可以方便且正确的被程序找到。
  3. 安全性,即权限,看本文的wiz居多,我们总不希望玩家可以随便执行巫师指令吧。。。那么我们就要想个办法,让适合的人能执行适合的指令,而不是相反。

请等待第二讲·下~~谢谢。


附件

第三讲:dbase和登录过程

这几天过10.1,反而比较忙,不常在家。

所以估计这八天里也就是今天这一讲了。

实际上lib的内容昨天就写完了。关键是怎么讲的问题。

第二讲里,本来说是准备接着把指令的权限部分完成,结果仔细想了一下,发现没法搞。原因有二:

  • 第一个是,还没有登录过程,登进来的user_ob都不知道谁是谁,如何定权限?
  • 第二个是,一旦说到登录,那事情就变得复杂了,最起码的,“是谁”的问题,也就是id怎么存到user_ob身上呢?这起码应该有个dbase。

所以想了半天,决定把这两块先补充上,然后再继续写权限的部分。

因此这一讲有两个重点,一个是dbase和相关的函数,另外一个就是做一个简单的登录过程。在这之后后再按顺序把save restore,security这些搞定,我们就可以继续回到指令权限这个内容上来了。

请看目录(对应的文件见附件newlib.0.3.tar.gz):

.
|-- adm
|  |-- daemons
|  |  |-- cmd_d.c
|  |  `-- login_d.c
|  `-- obj
|      |-- master.c
|      `-- simul_efun.c
|-- cmds
|  `-- usr
|      `-- test.c
|-- include
|  `-- globals.h
|-- log
|-- obj
|  `-- user.c
`-- std
    `-- body
        `-- user_dbase.c

重点1.dbase

我们依然秉承这样的原则:所有新添加的内容都是暂时够用就好,打底子的东西我尽量细致的讲。这块熟悉了,以后我们根据需要添加细节就容易了,那个时候反而可以一笔带过。

请看/std/body/user_dbase.c。一般来说,es2继承过来的lib都是爱用/feature/dbase.c这样的结构。我自己受到kok lib的影响比较大,所以倾向于用/std/body这样的结构,这个不是很关键的东西。大家只要知道,如果要对照,就去看/feature/dbase.c就好了(一般来说dbase又是从treemap继承过来的,不过我个人比较喜欢不超过两层的继承关系)。

dbase是什么?

如我们之前所讲过的,lpc并不是完全意义上的面向对象,他不支持对数据域的直接访问,因此这个dbase就是对ob身上的数据以及数据处理接口函数的一个集合。 这里我们这个lib第一次用到了继承(inherit),具体如果想了解,大家可以搜一下面向对象有关的说明,一般性的,只要知道被继承的文件里的数据和函数可以直接当作继承后的对象身上的数据和函数就好。

以我们现在使用的user_ob为例,我们总是需要知道、或者设置他“是谁”,“有多少hp“这样的问题,归纳起来,最常用的可能就是“哪个属性”是“多少”这样的话题了。对比lpc的数据类型,这正是mapping(映射)的拿手好戏。

所谓mapping,其实在更通用的数据结构书里,也就是hash表(哈希表)。这东西的内部机制不讲了,大家只要了解,他分为key和value两部分通常可以写成([“a”:1,“b”:2])这样的形式,并且可以用map[“a”]这样的形式来引用和赋值。

所以我们第一步就是给user_ob设计了一个叫做dbase的mapping,用来存储大部分的数据(事实上,包括ROOM,NPC,item,都是有类似的结构,一般的lib也都是继承了同样的dbase.c,但是我们这里暂时只给user_ob使用,因此叫做user_dbase.c,至于以后其他的对象是否也继承它、还是单独写各自的xxx_dbase,要根据需求再定)

具体请看user_dbase.c

mapping dbase = ([]);

mixed set(mixed prop,mixed data)
{
        return dbase[prop] = data;
}

mixed query(mixed prop)
{
        return dbase[prop];
}

void delete(mixed prop)
{
        map_delete(dbase,prop);
}

现在这个版本相当粗略,我们只定义了一个叫做dbase的“全局”mapping,和三个函数set(),query(),delete()。

事实上,真正使用的lib里,往往要比这个复杂的多,至少有三处:

  1. 多一个temp_dbase的static mapping(在不同的driver下,static也可能被写作nosave,表示不会被save_object()所整理存盘),用来存储那些只在对象存在过程中被使用,但是不需要存盘的数据;
  2. 至少还应该有一个add()函数(实际上就是set(xxx,query(xxx)+data))
  3. 这个dbase应该支持多层结构,也就是他的任何一个data都可以又是个mapping,这就是我们的prop用mixed来定义的原因

关于这个,我们多讲一句,set(“aaa/bbb”,xxx)这种结构大家可能都见过,这就是表示aaa这个key的内容是([“bbb”:xxx])。这会使得我们的这几个函数变得负责不好读,而且暂时也用不到,因此我们把这个需求留待以后再处理。现在我们只要能支持读写单纯的key = value形式就好了。

有了dbase,我们就可以读/写用户物件上的数据了。


重点2.登录过程

很遗憾,现在这个版本的lib,我们依然没法把它写完整。为什么呢?因为我们还没有save/restore机制。

请看user.c

int setup();

void create()
{
//      setup();
}
/*
string process_input(string arg)
{
        write(arg+"\n");
}
*/

// apply of user_ob :: logon()
// after master->connect(),mudos calls user_ob->logon()
// we use this to put user_ob into LOGIN_D
// and start Login authentication
void logon()
{
        call_out("login_timeout",60);
        this_object()->set("login_temp",1);
        LOGIN_D->logon(this_object());
}

void login_timeout()
{
        if(this_object()->query("login_temp"))
                destruct(this_object());
}

这里我们调整了一下,把原来在create()里的setup()过程干掉了,增加了一个叫做logon()的函数。

注意,这是一个apply,它在master apply::connect()之后会被调用。

我们用这个函数把已经获得了连线物件权限的user_ob发给了LOGIN_D。

void logon()
{
        call_out("login_timeout",60);
        this_object()->set("login_temp",1);
        LOGIN_D->logon(this_object());
}

注意这里有一个call_out,用来防止有用户连进来什么都不干,所以有个60秒的超时处理。然而为了防止正常登录用户也在60秒之后被踢。所以用一个login_temp的标记来做判断,这个标记会在LOGIN_D成功登录之后被清理掉。从而保证登录进来的用户不会被踢。(这里本来应该是个temp mark,不过咱们没有,就先凑合用dbase存)

未来/adm/daemons/login_d.c会被用来处理整个的登录握手或新用户创建过程。只是目前还是一个很粗糙的结构。

void logon(object ob)
{
        write("Welcome to my New mudlib\n");
        input_to("get_id", ob);
}

这是被user_ob的logon()所呼叫的处理函数,你看多好。。。连WELCOME都有了。。。。

我们直接用input_to获取用户输入的id。

正常过程,这里应该判断是否有该用户,用户id是否合法,然后是获取密码,对用户data做restore等流程。这里就简化了。

总之最后是进入到enter_world()过程,应该把用户丢到合适的room(可惜我们还没有room。。。),给用户适当的权限(我们也没有。。。),打开用户的指令输入权利(这个有了,就是user_ob→setup(),大家还记得吧,之前被我们从create()里干掉的setup()终于又被我们找了个机会用回来了)。

这里打一个补丁,setup()里我们增加了set_living_name()这个函数,是为了让find_player()可以找到这个“玩家”。

如此,一个完整的登录过程,虽然粗糙,但是结构有了。

下面我们就可以从容的完成权限分配,save/restore等内容了。。。请期待下一讲~

ps.随着lib逐渐搭起来,我也很犹豫,到底是把一些我们后来发展起来的比较新的模式放进去,还是尽可能贴近大家比较熟悉的老lib。

不过考虑到这个lib的讲解的性质,还是决定尽可能贴近老lib,毕竟大家用的熟了,更多还是在这些体系下写东西。


附件

第四讲

  1. 存盘和读取的底层机制
  2. 简单的房间系统和简单的look指令,物件的move
  3. 指令权限还是没有。。。。
  4. 登录过程里依然没有密码验证,但是可以区分新老用户了,并且可以存取数据。

这是本章的基本内容。具体请见附件。

详细的文字说明,我明天补上。

(抱歉我老婆发烧,这两天都在照顾她,晚上才赶出来的code,说明就实在来不及了。)

另外注一下,所有文件当中的中文都是utf8格式,大家用的时候可以批量转一下码(utf8>gb)

现在已经可以这样了:

欢迎来到测试MUD
请输入您的ID:akuma
读取存盘数据成功,开始进入游戏。
世界的中心 -
    这里就是世界的中心。人来人往,车水马龙。
到处都是人,摩肩接踵的。。。。非常热闹。
   
    这里有:
    akuma
> test连线进入这个世界。

目录结构如下:

.
|-- adm
|  |-- daemons
|  |  |-- cmd_d.c
|  |  `-- login_d.c
|  `-- obj
|      |-- master.c
|      `-- simul_efun.c
|-- cmds
|  `-- usr
|      |-- look.c
|      `-- test.c
|-- d
|  `-- wiz
|      `-- center.c
|-- data
|  `-- user
|      |-- a
|      |  `-- akuma.o
|      `-- t
|          |-- test.o
|          `-- test2.o
|-- include
|  `-- globals.h
|-- log
|-- obj
|  `-- user.c
`-- std
    |-- body
    |  |-- move.c
    |  |-- room_dbase.c
    |  |-- user_dbase.c
    |  `-- user_save.c
    `-- room.c

附件

course/mudlib的打造.txt · 最后更改: 2020/08/15 21:40 (外部编辑)