製作麥塊/我的世界啓動器
Java Minecraft Edition是最常見的Mc版本,其遊戲在設計時將遊戲分爲了⌈遊戲本體⌋與⌈啓動器⌋兩個部分,這使我們可以透過自行編寫啓動器支援第三方內容。
啓動器的基本原理
Minecraft透過 Java 虛擬機(JVM)運行遊戲,並需要附加一些遊戲的啓動參數。這些參數控制了 Minecraft 的行爲,如用戶資訊與遊戲資源。
就像運行普通的 Java 程式一樣,我們需要準備 JVM 參數 和 遊戲參數,並透過指令將它們傳遞給 Java 執行。
簡單範例
在C++中我們可以透過std::system()函式來執行shell命令,從而調用 JVM 來啟動 Minecraft。
啓動命令大致如下:
javapath -Xmx2G -cp "minecraft.jar" net.minecraft.client.main.Main --username <username> --version <version> --accessToken <access_token> --userProperties {} --assetsDir <assets_dir> --assetIndex <asset_index> --gameDir <game_dir> --width 854 --height 480Xmx2GJVM參數,設定該虛擬機可用的最大記憶體。"minecraft.jar"遊戲的主jar檔。net.minecraft.client.main.Main主類名,設定該java程式的入口。--username、--version、--accessToken等則是遊戲本體的參數。
程式碼範例(非完整):
#include <iostream>
#include <string>
#include <cstdlib>
void launchMinecraft(const std::string& javaPath, const std::string& gameDir, const std::string& username, const std::string& accessToken) {
std::string command = javaPath + " -Xmx2G -cp \"" + gameDir + "/minecraft.jar\" net.minecraft.client.main.Main "
"--username " + username +
" --version latest " + ...
" --accessToken " + accessToken +
" --gameDir " + gameDir +
" --width 854 --height 480";
std::cout << "Launching Minecraft with command: " << command << std::endl;
std::system(command.c_str());
}
int main() {
std::string javaPath = "C:/Program Files/Java/jre1.8.0_251/bin/java";
std::string gameDir = "C:/Minecraft";
std::string username = "playerName";
std::string accessToken = "abcdefg123456789";
launchMinecraft(javaPath, gameDir, username, accessToken);
return 0;
}
構建命令
我們如何構建命令?
通常來說,遊戲的啓動參數都存儲在相應的版本資料夾中。
列如: /to/path/.minecraft/versions/VerName/VerName.json。
其使用json格式存儲資訊:
檔案結構
版本資訊json
| 鍵名 | 類型 | 描述 |
| - | - | - |
| arguments | Map | 參數列表,分爲jvm與game參數,分別又爲兩個子列表。|
| assetIndex | Map | 該版本的資源索引資訊,包括版本id(字串,標識符)、url(字串)、sha1(字串)、size(整型)、totalSize(整型,所有資源總大小)等鍵。|
| javaVersion | Map | 要求的Java資訊,如:majorVersion(推薦JAVA版本)等。|
| libraries | Map | 遊戲依賴的程式庫,包括下載資訊。|
| downloads | Map | 客戶端與服務端的下載資訊。存儲在client與server鍵中,包括url、sha1、size鍵。
| logging | Map | log4j設定檔。|
| id | 字串 | 標識符,版本名|
| mainClass | 字串 | 主類名|
| jar | 字串 | 客戶端主檔名|
| assets | 字串| 資源版本|
| type | 字串 | 版本類型,可以是:release(正式版)、snapshot(快照)、old_beta(Beta版)或old_alpha(Alpha版)。|
| time | 字串 | 版本更新時間|
| releaseTime | 字串 | 版本發佈時間|
| minimumLauncherVersion | 整型| 要求的最低官方啓動器版本|
| patches | Map | 非官方結構,某些啓動器可能會用於存儲原版的啓動資訊。
規則(rules)
arguments和libraries每項都可能包含一個名爲rules的Map:
| 鍵名 | 類型 |描述 |
| - | - | - |
| action | 字串 | allow(允許) 或 disallow(不允許),如果滿足規則中的其餘條件則執行。
| features | Map | 對應啓動器內設定,通常是一個布林變數。如:is_demo_user。
| os | Map | 系統要求,name(系統名稱,osx、windows,linux)、version(系統版本)、和arch(系統架構,x86、arm)。
範例:
{
"name": "org.lwjgl:lwjgl-glfw:3.2.2",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-glfw/3.2.2/lwjgl-glfw-3.2.2.jar",
...
},
...
"rules": [
{
"action": "allow"
},
{
"action": "disallow",
"os": {
"name": "osx"
}
}
]
}按照上述rules,除了osx不添加該物件,其餘系統都應添加。
僅某個系統允許:
"rules": [
{
"action": "allow",
"os": {
"name": "osx"
}
}
]依賴程式庫
libraries中每個對象都應有name與downloads鍵,但需要注意downloads鍵值有可能爲空(無鍵值)。
鍵name遵循特定規則命名:
<package>:<name>:<version>package為完整包名,name為庫名,而version為庫版本號。
普通依賴程式庫的資訊存儲於libraries[i].downloads.artifact:
|鍵名|類型|描述|
|-|-|-|
|path|字串|該程式庫相對與libraries資料夾的路徑,包括副檔名。|
|url|字串|下載該程式庫的完整url。|
|sha1|字串|雜湊值|
|size|整型|該檔案的大小|
由於downloads並非一定存在,所以應當以name鍵構建路徑。
package與name中的.在構建路徑時應替換爲資料夾層級/,版本中的保持不變。檔案名稱則爲<name>-<version>.jar。
舉例來說,name爲org.ow2.asm:asm:9.1解析路徑爲:org/ow2/asm/asm/9.1/asm-9.1.jar。
一步步來,org.ow2.asm是包,其中的.應替換爲資料夾層級,org/ow2/asm。
再把:替換爲/就得到了路徑,org/ow2/asm/asm/9.1,加上檔案名:/asm-9.1.jar鍵就得到最終路徑:org/ow2/asm/asm/9.1/asm-9.1.jar了。
當然,你還需要拼接libraries的絕對路徑才是完整。列如:/to/path/.minecraft/libraries/org/ow2/asm/asm/9.1/asm-9.1.jar。
natives程式庫
natives用來支援特定平台,其可以透過libraries[i].downloads.natives確定,如果沒有該鍵則認爲不存在。
natives存儲於libraries[i].downloads.classifiers.<natives_key>。
<natives_key>為natives鍵中系統對應的值,範例:
{
"name": "org.lwjgl:lwjgl-jemalloc:3.2.1",
"downloads": {
"artifact": {
...
},
"classifiers": {
"natives-macos": {
...
}
}
},
"natives": {
"osx": "natives-macos"
},libraries[i].downloads.classifiers.natives-macos爲該natives程式庫資訊。
它的檔案路徑與一般程式庫大致相同,只是檔案名爲:
<name>-<version>-<natives_key>.jar也就是說,如果一個程式庫路徑爲org/lib/hi-lib.jar並且存在對應系統的netives庫,那麼其路徑應爲:org/lib/hi-lib-netives-macos.jar。
啓動參數解析
啟動參數將傳入java或javaw可執行檔,並分為JVM參數和Minecraft參數兩部分。
注,一般你只需要從版本檔案中解析出參數並替換相應參數即可。無需手動關心添加。
以下爲參數解析:
JVM參數
-X、-XX:組態JVM,如GC。-Xmx1024m設定該虛擬機可用的最大記憶體為1024MB。-XX:-OmitStackTraceInFastThrow省略異常棧資訊從而快速拋出。
-D:組態JVM系統屬性,格式為-D<name>=<value>-Dos.name=Windows 10 -Dos.version=10.0設定系統名與版本。-Dminecraft.launcher.brand=NekoLauncher -Dminecraft.launcher.version=1.0.1目前啓動器的名稱與版本。如果有空格或特殊字符,需要使用引號包裹。-Dlog4j.configurationFile=<檔案路徑> e.g /client-1.12.xml遊戲日誌設定檔-Djava.library.path=<natives資料夾路徑>當前系統下遊戲運行所需的natives程式庫。
-cp:Class Path- 所有當前版本Minecraft的
普通庫檔案路徑及遊戲主檔,路徑在Windows使用;分隔,Unix使用:。
- 所有當前版本Minecraft的
arguments.jvm中可能需要的參數,在解析命令時替換:
|鍵名|描述|
|-|-|
|${natives_directory}| natives程式庫的資料夾,官方啓動器會將其解壓至臨時資料夾,你可以自行拷貝程式庫到固定位置。|
|${launcher_name}|啓動器名稱,包括特殊字串請使用引號包裹。|
|${launcher_version}|啓動器版本,類型爲字串。|
|${classpath}|遊戲主檔(如1.20.jar)與所有一般程式庫的絕對路徑。|
|${library_directory}|libraries資料夾的絕對路徑。
遊戲參數
以主類名開頭,通常為net.minecraft.client.main.Main,若安裝Mod載入器則一般為net.minecraft.launchwrapper.Launch 或cpw.mods.modlauncher.Launcher。
你只需要關心遊戲版本檔中獲取即可。
常見參數:
- --username 後接使用者名稱。
- --version 後接遊戲版本。
- --gameDir 後接遊戲路徑。
- --assetsDir 後接資源檔案路徑。
- --assetIndex 後接資源索引版本。
- --uuid 後接使用者UUID。
- --accessToken 後接登入權杖。
- --userType 後接使用者類型。
- --versionType 後接版本類型,會顯示在遊戲主介面右下角。
- --width 後接視窗寬度。
- --height 後接視窗高度。
- --server 後接伺服器位址,遊戲進入時將直接連入伺服器
- --port 後接伺服器的埠號
- ${**} 由啓動器填入的參數
arguments.game中可能需要的參數,在解析命令時替換:
|鍵名|描述|
|- | -|
|${auth_player_name}|使用者名稱|
|${version_name}|版本名稱,可任意字串|
|${game_directory}|遊戲的絕對路徑,如:/to/path/.minecraft。
|${assets_root}|資源檔的根路徑,如:/to/path/.minecraft/assets。
|${assets_index_name}|資源版本,如1.20,解析assets鍵填入。
|${auth_uuid}|uuid
|${auth_access_token}|登入權杖Token
|${user_type}|mojang
|${version_type}|版本類型,可任意字串
在C++中實現
可以參考我已經在Neko Launcher中實現的部分。
後面我將會再編寫一篇新的文章專門講解在C++中實現的思路,以下是大致思路解決方案:
我們假定遊戲資料夾位於/to/path/.minecraft/
讓我們獲取遊戲版本:
//使用資料夾迭代,檢查資料夾中的所有內容
for (const auto &it : std::filesystem::directory_iterator("/to/path/.minecraft/versions/")) {
//如果是目錄,則代表這是一個遊戲版本
if (it.is_directory()) {
verDir = std::filesystem::absolute(it).string();//取得絕對路徑
verFile = verDir + "/" + it.path().filename().string() + ".json";//資料夾名即爲版本檔案名
}
}
解析json
...檢查條件(第三方程式庫與定義參閱原始碼):
auto checkCondition = [=](const RulesMap &rules, const nlohmann::json &features) -> bool {
if (!features.empty()) {
if (features.contains("is_demo_user") && features["is_demo_user"].get<bool>() == isDemoUser)
return true;//僅鍵存在且與變數isDemoUser相同的情況下返回true。
if (features.contains("has_custom_resolution") && features["has_custom_resolution"].get<bool>() == hasCustomResolution)
return true;
}
if (!rules.osName.empty()) {
bool allow = (rules.osName == info::getOsNameS() && rules.action == "allow") || (rules.osName != info::getOsNameS() && rules.action == "disallow");
if (allow)//系統名相同且規則爲允許,或不相同且規則爲禁止時返回true
return true;
}
if (!rules.osArch.empty()) {
bool allow = (rules.osArch == osArch && rules.action == "allow") || (rules.osArch != osArch && rules.action == "disallow");
if (allow)
return true;
}
return false;
};processArgs思路:
迭代arguments.jvm與arguments.game,判斷爲字串則直接添加。
如果爲Map或物件,則嘗試迭代rules。
name構造路徑:
使用find查找三次:或使用正則std::regex("([^:]+):([^:]+):([^:]+)")捕獲。
然後拼接:
return package + "/" + name + "/" + version + "/" + name + "-" + version + ".jar";替換參數:
// replace placeholders
auto replacePlaceholders = [&](std::vector<std::string> &argsVec, const std::map<std::string, std::string> &placeholders) {
for (auto &arg : argsVec) {
for (const auto &[key, value] : placeholders) {
std::string::size_type pos;
while ((pos = arg.find(key)) != std::string::npos) {
arg.replace(pos, key.length(), value);
}
}
}
};
// jvm
replacePlaceholders(jvmArgsVec, {{"${natives_directory}", nativesPath},
{"${library_directory}", librariesPath},
{"${launcher_name}", "Neko Launcher"},
{"${launcher_version}", info::getVersion()},
{"${classpath}", classPath}});
// game
replacePlaceholders(gameArgsVec, {{"${auth_player_name}", gameArgsName},
{"${version_name}", gameArgsVerName},
{"${game_directory}", gameDir},
{"${assets_root}", gameArgsAssetsDir},
{"${assets_index_name}", gameArgsAssetsId},
{"${auth_uuid}", gameArgsUuid},
{"${auth_access_token}", gameArgsToken},
{"${user_type}", gameArgsUserType},
{"${version_type}", gameArgsVerType}});將所有內容拼接後,可以使用std::system調用命令。如果在windows上拼接後的內容過長(8000於字符),你可能需要調用powershell來啓動。
更詳盡的內容可以參閱Neko Launcher。
部分參閱來源:
太技术了。咋一看像是天书,仔细一看,这特么不是天书是神马
It's very easy to find out any matter on web as compared
to textbooks, as I found this ppiece of writing at this website.