系統刷新與內存清除分析


  有關系統更新一直是玩家乃至于新巫師們關心的問題。比如,為何每隔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
}

  我們再次復習一下clean_up()函數返回1的含義,如果clean_up()函數返回1,則MUDOS在這一次的調用時不會做其的任何舉動,但到了下一次想調用的時間里,還將再次調用這個對象的clean_up()函數。那么從這可以看出,有以下四種情況不會將其清除出內存:
一、非clone出來并且有no_clean_up參數的對象﹔
二、玩家永遠不會
三、處于一個還存在的環境里
四、自己里面存在著玩家
  也就是MUDOS定時摧毀內存不需要的對象是由外向內的,比如一個房間,系統只要檢查這個房間里沒有no_clean_up參數、里面沒有玩家就可清除它,而房間里的物品、NPC都會因環境的不存在而消失。這個清除的定時時間一般都為兩個小時。當然要視不同的MUDOS里的設置而看的。
  再說一點題外話,如果一個房間長時間沒有玩家走進來,當然會被MUDOS清出內存,而突然又有玩家進來呢?很簡單,它會在一瞬間被編譯進內存,進入一個已經存在在內存里的房間與進入一個剛剛編譯出來進入內存的房間對于我們的玩家來說,是察覺不出它們之間的差異的。