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
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM