尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️
文/Tiger Tang
這個問題太想自問自答了,因為這段血淚史完全可以寫成精彩紛呈的長篇小說!作為在 Minecraft 業界打滾多年的人,必須得給大家侃侃背後的故事!
// Survival Multiplayer 時代(2010)
讓時光回溯到五年前的 8 月 9 日的凌晨。我們的故事主角兼 Minecraft 創始人,Markus ‘Notch’ Persson,正二十四個小時宅在家里,撐著雙眼死瞪電腦螢幕,雙手則 迅速地敲著鍵盤,廢寢忘食地調試著程序。再過一個小時就是 8 月 10 日了,Minecraft 生存多人遊戲(Survival Multiplayer,SMP)正式發布的日子。
你看 Notch 這妖魅的小眼神
Minecraft SMP 的名字聽上去很高大上,但其實就是一個叫做 minecraft_server.jar 的文件罷了,小巧綠色又便攜。使用方法也非常簡單,雙擊打開,它就會自動在默認端口上設置好一個 Minecraft 的服務器,別人只需憑你的 IP 即可進入。理所當然地,一些基本的命令也包含在其中:/kick 用來踢人,/gamemode 用來從生存轉創造…
現在看來,第一個版本的 SMP 相當簡陋,但玩家們正沉醉於和朋友一起玩生存的樂趣里,再簡陋也讚不絕口。
SMP 的發布,正是迎合 Minecraft 迅猛上升的用戶註冊量。截至 2010 年 5 月,Minecraft 的付費用戶已經達到兩萬,YouTube 上以 Minecraft 作為關鍵字的視頻日益增長,而此時,這個遊戲還只是在 Alpha 階段!
而 SMP 的出現,更讓 Minecraft 的知名度登上又一巔峰:什麼!?可以和好基友在開放式的 LEGO 世界里生存、探險、搞基(?);還可以開創造起個鬥獸場戰個痛或者堆滿 TNT 然後炸地圖;最給力的是神似編程的紅石系統,直接令 Minecraft 一躍成為遊戲開發工具!哪個 AAA 級遊戲有這麼爽的體驗?!
即使從五年後看來,SMP 的第一個版本也有相當高的遊戲性
在 SMP 發布僅僅兩個月後,Minecraft 的付費用戶就翻了個 1.5 倍,兩個月就賺了一百多萬!SMP 的巨大成功並沒有讓 Notch 怠慢,沒過多久就向玩家們宣布了 Beta 版本的到來。而 Notch 也正式註冊了 Mojang AB 的商標,為之後發行遊戲鋪路。
// hMod 時代(2010 ~ 2011)
SMP 好玩歸好玩,可是不能在上面裝 mod 這一點讓不少玩家很苦惱。當然了,可以通過反編譯 minecraft_server.jar 修改里面的代碼,比如調整一下玩家的默認速度什麼的,然後每個玩家一走起路來就跑十公里遠,上天入海不是夢。畢竟 Mojang 也沒有做什麼簽名驗證,也沒什麼坑爹的全程聯網驗證(育碧:…),要修改幾個變量然後重新編譯,理論上來講不難啊。
可行歸可行,問題是修改起來太麻煩:代碼全部被混淆(obfuscated)了!
什麼叫代碼混淆呢?舉個栗子,比如說原本的代碼是這樣的:
private String playerName = "你爸爸"; // 定義玩家名稱 private double health = 20.0D; // 定義玩家血量 private float walkSpeed = 1.2F; // 定義玩家速度 public void chat(String message) { // 定義一個說話的函數 Server.broadcastMessage(this, message); // 向服務器里的函數傳遞參數 }
沒學過 Java 是不是也很清晰明了?這修改起來還不容易,簡直就是填空嘛,小學生都會。
問題是在編譯的時候,代碼被 Mojang 事先混淆了,可能到你手里的時候就變成這樣了:
String a = Base64.decodeFromBase64("5L2g54i454i4"); double b = 20.0D; float c = 1.2F; public void d(String a) { bl.aE(this, a); }
尼瑪這叫一個狠哪,若是沒有原本的代碼,你看得懂嗎?
你或許說,上面這幾行,我也能猜出個大概吧?嗯,b 是血量,因為玩家血量最高就是 20,然後 c 是… bl 是… aE 是…
別忙著翻桌,我們再來看看真實個例,下面是 Minecraft 1.8 里面的 aap 類:
public class aap extends um { private static final Logger b = ; public float a = (float) (Math.random() * 3.141592653589793D * 2.0D); private int c; private int d; private int e = 5; private String f; private String g; public aap(amp paramamp, double paramDouble1, double paramDouble2, double paramDouble3) { super(paramamp); a(0.25F, 0.25F); b(paramDouble1, paramDouble2, paramDouble3); this.y = ((float) (Math.random() * 360.0D)); this.v = ((float) (Math.random() * 0.2000000029802322D - 0.1000000014901161D)); this.w = 0.2000000029802322D; this.x = ((float) (Math.random() * 0.2000000029802322D - 0.1000000014901161D)); } public aap(amp paramamp, double paramDouble1, double paramDouble2, double paramDouble3, aio paramaio) { this(paramamp, paramDouble1, paramDouble2, paramDouble3); a(paramaio); } public aap(amp paramamp) { super(paramamp); a(0.25F, 0.25F); a(new aio(apg.a, 0)); } protected boolean q_() { return false; } protected void g() { F().a(10, 5); } public void j() { if (k() == null) { H(); return; } super.j(); if ((this.d > 0) && (this.d != 32767)) { this.d -= 1; } this.p = this.s; this.q = this.t; this.r = this.u; this.w -= 0.03999999910593033D; this.T = j(this.s, (aL().b + aL().e) / 2.0D, this.u); d(this.v, this.w, this.x); int i = ((int) this.p != (int) this.s) || ((int) this.q != (int) this.t) || ((int) this.r != (int) this.u) ? 1 : 0; if ((i != 0) || (this.W % 25 == 0)) { if (this.o.p(new dl(this)).c().r() == big.i) { this.w = 0.2000000029802322D; this.v = ((this.V.nextFloat() - this.V.nextFloat()) * 0.2F); this.x = ((this.V.nextFloat() - this.V.nextFloat()) * 0.2F); a("random.fizz", 0.4F, 2.0F + this.V.nextFloat() * 0.4F); } if (!this.o.C) { v(); } } float f1 = 0.98F; if (this.C) { f1 = this.o.p(new dl(sr.c(this.s), sr.c(aL().b) - 1, sr.c(this.u))).c().K * 0.98F; } this.v *= f1; this.w *= 0.9800000190734863D; this.x *= f1; if (this.C) { this.w *= -0.5D; } if (this.c != -32768) { this.c += 1; } if ((!this.o.C) && (this.c >= 6000)) { H(); } } private void v() { for (aap localaap : this.o.a(aap.class, aL().b(0.5D, 0.0D, 0.5D))) { a(localaap); } } private boolean a(aap paramaap) { if (paramaap == this) { return false; } if ((!paramaap.ad()) || (!ad())) { return false; } aio localaio1 = k(); aio localaio2 = paramaap.k(); if ((this.d == 32767) || (paramaap.d == 32767)) { return false; } if ((this.c == -32768) || (paramaap.c == -32768)) { return false; } if (localaio2.b() != localaio1.b()) { return false; } if ((localaio2.n() ^ localaio1.n())) { return false; } if ((localaio2.n()) && (!localaio2.o().equals(localaio1.o()))) { return false; } if (localaio2.b() == null) { return false; } if ((localaio2.b().k()) && (localaio2.i() != localaio1.i())) { return false; } if (localaio2.b < localaio1.b) { return paramaap.a(this); } if (localaio2.b + localaio1.b > localaio2.c()) { return false; } localaio2.b += localaio1.b; paramaap.d = Math.max(paramaap.d, this.d); paramaap.c = Math.min(paramaap.c, this.c); paramaap.a(localaio2); H(); return true; } public void i() { this.c = 4800; } public boolean T() { return this.o.a(aL(), big.h, this); } protected void f(int paramInt) { a(ua.a, paramInt); } public boolean a(ua paramua, float paramFloat) { if (b(paramua)) { return false; } if ((k() != null) && (k().b() == aip.bU) && (paramua.c())) { return false; } X(); this.e = ((int) (this.e - paramFloat)); if (this.e <= 0) { H(); } return false; } public void b(eu parameu) { parameu.a("Health", (short) (byte) this.e); parameu.a("Age", (short) this.c); parameu.a("PickupDelay", (short) this.d); if (m() != null) { parameu.a("Thrower", this.f); } if (l() != null) { parameu.a("Owner", this.g); } if (k() != null) { parameu.a("Item", k().b(new eu())); } } public void a(eu parameu) { this.e = (parameu.e("Health") & 0xFF); this.c = parameu.e("Age"); if (parameu.c("PickupDelay")) { this.d = parameu.e("PickupDelay"); } if (parameu.c("Owner")) { this.g = parameu.j("Owner"); } if (parameu.c("Thrower")) { this.f = parameu.j("Thrower"); } eu localeu = parameu.m("Item"); a(aio.a(localeu)); if (k() == null) { H(); } } public void d(adq paramadq) { if (this.o.C) { return; } aio localaio = k(); int i = localaio.b; if ((this.d == 0) && ((this.g == null) || (6000 - this.c <= 200) || (this.g.equals(paramadq.b_()))) && (paramadq.bg.a(localaio))) { if (localaio.b() == ahw.a(apg.r)) { paramadq.b(rl.g); } if (localaio.b() == ahw.a(apg.s)) { paramadq.b(rl.g); } if (localaio.b() == aip.aA) { paramadq.b(rl.t); } if (localaio.b() == aip.i) { paramadq.b(rl.w); } if (localaio.b() == aip.bq) { paramadq.b(rl.A); } if ((localaio.b() == aip.i) && (m() != null)) { adq localadq = this.o.a(m()); if ((localadq != null) && (localadq != paramadq)) { localadq.b(rl.x); } } this.o.a(paramadq, "random.pop", 0.2F, ((this.V.nextFloat() - this.V.nextFloat()) * 0.7F + 1.0F) * 2.0F); paramadq.a(this, i); if (localaio.b <= 0) { H(); } } } public String b_() { if (i_()) { return aG(); } return eq.a("item." + k().a()); } public boolean az() { return false; } public void c(int paramInt) { super.c(paramInt); if (!this.o.C) { v(); } } public aio k() { aio localaio = F().f(10); if (localaio == null) { if (this.o != null) { b.error("Item entity " + D() + " has no item?!"); } return new aio(apg.b); } return localaio; } public void a(aio paramaio) { F().b(10, paramaio); F().h(10); } public String l() { return this.g; } public void a(String paramString) { this.g = paramString; } public String m() { return this.f; } public void c(String paramString) { this.f = paramString; } public void o() { this.d = 10; } public void p() { this.d = 0; } public void q() { this.d = 32767; } public void a(int paramInt) { this.d = paramInt; } public boolean r() { return this.d > 0; } public void t() { this.c = -6000; } public void u() { q(); this.c = 5999; }}
能猜得出來算你狠。
於是,雖然 SMP 的第三方修改成為可能,但基本沒有服主會閒的蛋疼去玩這個。除了代碼被混淆之外,由於 Minecraft 長期都是 Notch 一個人開發,所以內部的業務邏輯也寫得很亂,或者說實在太有 Notch 特立獨行的代碼風格了,窩們實在猜不粗來呀!
Notch 表示:「你丫反編譯我的代碼還瞎逼逼」(設計對白)
不過就是有些人點錯天賦了,就在 SMP 發布後沒多久的 2010 年年底,一位叫 hey0 的大神在自己的個人網站上發布了 hMod。hMod 一出,激起千層浪,眾人紛紛驚呼:民間奇才!
hMod 是個什麼玩意兒?我盡量簡單地解釋一下。以往的 SMP modding 模式(也就是上面提到的,直接修改源代碼),我們畫個流程圖出來:
hMod 的原理,就是將那些不可讀的代碼,通過 hey0 君敏銳的觀察能力,「翻譯」成可讀而清晰明了的東西。
還記得剛才那堆亂七八糟的代碼嗎?有興趣的同學可以自行閱讀「翻譯」過後的代碼。
(「翻譯」這詞實際上並不準確,實際上 hMod 是對 SMP 的半封裝,詳細的技術細節在此略過。)
這實在太偉大了!要在服務器上加入自己原創的內容,頓時簡單了起來。
不過如此偉大的 hMod 更新了幾個月,原作者就突然潛水,小道消息是說回老家結婚去了,然後由另一位現已就職 Mojang 的大神 Dinnerbone 繼續填坑。還沒填到一半 Dinnerbone 就不幹了:靠,代碼真亂!於是拉上幾個志同道合的同志一起推翻重做,扛起「翻譯」的任務,Bukkit 計劃就這麼誕生了。
// Bukkit 時代(2011 ~ 2014)
Bukkit 計劃實際上分為兩部分:Bukkit API 和 CraftBukkit。廢話不多說,我們再畫個流程圖:
原理和 hMod 是一樣的,但 Bukkit API 寫得更好之餘,最重要的成就就是加入了事件系統,不過這個話題說下去完全可以另起爐灶了,所以咱們暫且跳過。
好了我知道你們都在吐槽上面的魔法是什麼鬼,那麼我盡量簡單講一講,沒有面向對象編程基礎的同學可以跳過下面這幾段。
Bukkit API 里全部都是抽象的類與方法,打個比方有個方法叫 getOnlinePlayers(),返回當前玩家數量。為什麼要抽象?為什麼我們不直接整合做到(implementation)?比如我發現下面這行代碼就可以返回當前玩家數量,這不搞定了嗎,分兩步幹嘛。
aJ.e();
問題是我們的這行代碼的基礎,是通過破解 Minecraft SMP 的源代碼對吧?更準確的說,是通過破解 Minecraft SMP 當前版本的源代碼作為基礎。而代碼混淆這個過程,是每個版本都會重新進行一次的。上面那行代碼或許在 Minecraft SMP 1.7 能用,但到 1.8,可能就完全報錯了。因為或許在 1.8 里,要獲取當前玩家數量的代碼是這樣的:
b.aX();
所以,在 Bukkit API 的部分里,這個方法是抽象的,留給相應版本的 CraftBukkit 去做到。並且這麼一來,有了抽象的接口作為參考,新版本的 SMP 發布時,Bukkit 團隊也能更方便地更新 CraftBukkit。在這里也順便吐槽一下,常常見到有人說用 Bukkit 開服,其實是錯的 —— Bukkit 里全是抽象的接口而已,開個鬼啊。正確的說法是用 CraftBukkit 開服(其他服務器端另計)。所以下次你見到誰跟你炫耀說「我會用 Bukkit 開服務器你造嗎」,記得高大上的回他一句:「乖,那個叫 CraftBukkit。跟我讀,科阿哇夫特巴可以特。」
好了話題扯遠了,那麼有了 Bukkit 能做些什麼呢?能做的事太多了!比如用 Bukkit API 的自定義命令功能,加個叫 /launch 的命令,然後輸入 /launch <誰誰誰> 就將目標玩家噴上天,這無論在原生 Minecraft 里或者 SMP 里都是做不到的!
效果請參見左下角~ 這些基於 Bukkit API 的小程序被統稱為插件(plugin)
好了,我知道你們又要吐槽了。
當然不是!這種插件實在太膚淺了,Bukkit API 真正最廣泛的應用是用來開發小遊戲(minigame)。你沒聽錯,在遊戲里開發遊戲!只要有足夠的人力物力,依靠著 Bukkit API,要弄出個 Minecraft 版《無主之地》或者《使命召喚》是絕對可行的!
國外知名服務器 Hypixel 最近推出的新遊戲 Warlords,武器到裝備的模型都是完全自制的。Warlords 的核心玩法其實就是搶旗,但又加入了武器收集,附魔系統和角色系統等等,目前平均在線玩家 2000+,稱其為小型 PvP 網遊也絕不為過。
毫不誇張地說,Warlords 甚至要比 Steam 上不少免費的 FPS 好玩;光是收集要素就足夠吸引了!
另一知名大服 Wynncraft,則主打 RPG 玩法,照搬了當今網路上 MMORPG 的很多元素:饒有趣味的任務,廣闊宏偉的地圖,專門刷經驗升級的地城… 倒也弄得趣味橫生。
我們還是先回到 2011 年,回到 Bukkit 剛剛發展起來的時候吧:那時大眾對 Minecraft 多人遊戲的概念,還只是停留在與好基友一起玩生存的程度。真正將 Bukkit 計劃推向大眾視野的,當屬在 2011 年發展起來的 MCSG(Minecraft Survival Games)服務器。聽過《饑餓遊戲》吧?熟悉里面的設定吧?而 MCSG,就是饑餓遊戲在 Minecraft 的翻版:24 個玩家在開放式的地圖里生存,到處開箱尋找物資,誰生存到最後就贏。SMP 可能也弄得出來,但是想做複雜一點,將箱子的物品完全隨機化,或者將玩家數據保存在 mySQL 數據庫里,又或者加入一堆炫目的技能,那是 SMP 絕不可及的。而以上這一切,利用 Bukkit API,小 case 啦。
當然 Survival Games 遠遠沒有如今的 Warlords 吸引,但是在當年卻可謂掀起了一陣 Minecraft 潮。在 YouTube 上實況大型服務器里遊戲的實況主越來越多,即使是如今已經超過千萬訂閱的 SkyDoesMinecraft —— 就是那個玩了 Flappy Bird 的小夥 —— 也是玩 Minecraft 發家的(好吧,這個看名字就知道)。
好了,我知道你看到這里有點無聊了。所以下面重點來了!!v(。・ω・。)ィェィ♪
因為也是在 2011 年,一個名為 Buycraft 的東東進入了服主們的視野。Buycraft 是一個 CMS 系統,服務器的玩家可以通過在前端用 Paypal 或者信用卡購買東西,來獲得服務器上的增值服務。用 Minecraft 賺錢不再是夢!
真是爆炸性的大新聞。
……
(超燃 BGM 響起)
這豈止是爆炸性,簡直是歷史性!!!這不正是現在手遊最喜歡加入的課金系統嗎?!!
你嫌你的裝備太差嗎??
你覺得打怪升級太慢嗎???
你羨慕那些滿身神裝的高富帥嗎????
快來買 VIP 會員吧!!!!!!
一個月只需 10 美刀,即可讓你得到最尊貴的享受!!!!!!!!!!!
雙倍金錢!!!
三倍經驗!!!!
換裝系統!!!!!
寵物陪伴!!!!!!
房間防踢!!!!!!!
非 VIP 說話全是灰色的!!!!買了 VIP 你就算罵髒話我們也幫你加個白色高亮!!!!!!還有高端洋氣上檔次 VIP 字樣的前綴!!!!!
別人死了最多就一句死亡信息!!!!!!!!你死掉我們在你死亡的地點放個七彩煙花!!!!!!!!!讓整個世界都知道你死了!!!!!!!!!!!
於是,Minecraft 的多人遊戲到這里已經完全發展起輕工業來了,首先是開發難度低兼成本小,插件還不一定要自己開發,網上現成的一堆,實在不行開價讓別人來做;然後就等著收錢吧,五十美刀一個月的 VIP 照樣有人買!
Bukkit 計劃也從此聲名大噪,越來越多的熱心人士加入了開發行列,每一個更新都是無數服主歡呼的時刻。和 SMP 一樣簡單的開服流程也讓服務器越來越多,保守可能也有幾十萬 —— 甚至有了 Minecraft Server List,Minecraft Servers 這樣的網站,只是列出互聯網上公開的服務器地址,同時暗中通過競價為某些服務器提升排名,一個月就能賺上萬。
吃驚吧?更恐怖的在後頭呢。剛才提到過的,2012 年迅速崛起的小遊戲服務器 Hypixel,也是通過增值內容付費的方式,賺了個盆滿缽滿!根據我一位認識 Hypixel 開發者的外國朋友的可信消息,高峰期的 Hypixel,日均 30000+ 玩家,一年的淨利潤 $1000000+。除以 12,一個月九萬多美元,也即五十七萬人民幣。
一個月五十七萬。
一個月五十七萬。
一個月五十七萬。
一個月五十七萬。
一個月五十七萬。
可能有人會質疑以上的數字,那麼我曬曬親身經歷吧:學生黨一枚,有多年編程基礎,閒著沒事也寫寫插件。我曾經在 2013 年 9 月在一家名為 Minecade 屬下的 SkyDoesMinecraft 服務器工作過三個月,月薪一千。2014 年年尾幫 ArkhamNetwork 服務器做過外包項目,六百。
還有一些零零碎碎的小項目,在此不表。而無論是 Minecade 還是 ArkhamNetwork,甚至還擠不進大服務器的行列。可以想像得見一線服務器的員工們,一個月能賺多少了!
所以說如今 Minecraft 的多人遊戲完全是一條成熟的產業線,服主帶著充足的資金聘請員工,為服務器開發高質量內容吸引玩家;玩家則購買增值內容甚至主動捐款來令服務器盈利。這樣的良性循環在整個遊戲界來講都是很難得的。
與此同時,單人遊戲的體驗也在穩速提高,1.5 的紅石更新,1.6 的馬匹更新,1.7 的世界觀更新,而勤快追上步伐的 Bukkit 計劃又讓服務器們得以爭先搶後地在 CraftBukkit 新版本發布的第一時間更新服務器上的內容,為的就是吸引人流。
原本就蓬勃的 Minecraft 遊戲界在 2013 年進入了黃金時代。有充足的利潤打底,服務器們開發的新內容一次比一次高質量,Quakecraft、Hide and Seek、Prison、Factions、Arcade 等遊戲模式的名字已經深入民心,玩家們也樂意付錢,於是兩邊和樂融融,近年來遊戲業流行的 Freemium 的模式竟然在他們身上得到最好的做到。Minecraft 在這一年突破 1000 萬銷量,很大程度上要歸功於辛勤的服主們。
黃金時代終究是要過去的。狂喜的人們似乎沒有發現,一朵烏雲已經慢慢逼近……
// 輝煌背後
Bukkit 時代看似輝煌,但實際上有不少隱患出現:
第一是 Bukkit 本身的衰落。2012 年 2 月,Bukkit 的開發團隊(Dinnerbone,EvilSeph,Grum,Tahg)收到來自 Mojang 的 offer,於是欣然應邀加盟 Mojang;作為條件,他們不能再開發 Bukkit,而是負責開發新版本的 SMP 和其他與 Minecraft 有關的工作,比如編寫 Plugin API。
Dinnerbone 和 Grum 這兩位可以說是對整個 Bukkit 計劃貢獻最大的人,反編譯和反混淆由 Grum 全權負責,然後 Dinnerbone 則接過代碼坐在電腦桌前除了上廁所外不停歇地碼上二三十個小時(這就是愛啊 <3),為的就是以最快的速度將新版本的 Bukkit API 和 CraftBukkit 呈現在大眾面前。如今他們走了,雖然有人接班,但是他們都沒有了 Dinnerbone 和 Grum 的那份旁人難以理解的激情,更新對他們來說更像是一份義務而不是責任。這也不能怪他們,但伴之而來的就是 CraftBukkit 的更新越來越慢,當初兩天就能更新完,現在要花上兩個月;而Bukkit 在 1.5 後鮮有再加入新的 API,意思就是上文提到的「翻譯」活越來越少人肯去做,導致許多 SMP 的新功能都無法單純地利用 Bukkit API 做到,必須還得配合之前提到的那種直接修改源代碼的蛋疼方法…
作為過來人,我可以肯定地告訴你們:閱讀 Minecraft 的源代碼太蛋疼了…
第二是收費泛濫。服務器們收費的方式推陳出新,以 Hypixel 為例,VIP 出完了出 VIP+,VIP+ 出完了出 MVP,MVP 出完了再出 MVP+…
幾十美金幾十美金地收… 國內一線 MMO都沒這麼貴啊
就算玩家們樂意,他們的家長也不樂意呀!不少熊孩子一個月花了幾千美刀在 Minecraft 上,而家長們又怎會了解 Bukkit 服務器們的商業模式,於是出現了家長們憤怒地在推特上向 Notch 投訴並要求全額退款,否則要將 Mojang 告上法庭的啼笑皆非的情況。
Mojang 躺著也中槍:關我屁事啊!?
第三是版權問題。CraftBukkit 內置了 Minecraft 反編譯過後的源代碼,無形中已經侵犯了 Mojang 的版權;更搞笑的是,Bukkit 計劃採用的是 GPL 協議!一個開源計劃里卻包含了反編譯過的商業代碼,這一點本身能夠不被大眾口誅筆伐實屬幸運。
Mojang 當然知道 Bukkit 計劃是怎麼回事,不過他們對這些第三方服務器端也就是睜一只眼閉一只眼,只要你不把 Minecraft 重新打包一次就拿出去賣,你改成 Q 塊世界我也不管你。
我知道你們還沒完全消化這幾段的內容 (o´Д`)=з 但敬請記住這幾個關鍵詞吧:Bukkit 衰落、收費泛濫和版權糾紛。
因為故事要進入高潮階段了。
淡定,淡定
(待續……)