各位潛行者們,大家好!隨著《浩劫殺陣》三部曲的強化版(Enhanced Edition)推出,相信許多老玩家都回鍋重溫這款經典。如果你擁有原始的舊版遊戲,就可以在 Steam 上免費安裝強化版。而今天,這篇教學將帶領大家一步步為《浩劫殺陣:車諾比之影》強化版(SoCEE)打造一個功能強大的「願望實現器」(Wish Granter),也就是大家俗稱的作弊選單。
這篇教學屬於技術型文章,會需要修改到遊戲的腳本檔案。不過別擔心,只要跟著步驟一步一步來,即使是新手也能成功!讓我們開始動手,學習如何自己製作遊戲 MOD 吧!
遊戲版本資訊
- 遊戲名稱:S.T.A.L.K.E.R.: Shadow of Chornobyl - Enhanced Edition (簡稱: SoCEE)
- 遊戲版本:1.7.2
- 遊戲執行檔:xrEngine.exe / xrGame.dll
- 檔案版本:xrEngine.exe -- 1.7.2.13762 (1.7.2+28-3879775)
第一步:解包遊戲資料
三款強化版的遊戲引擎都是基於《浩劫殺陣:普里皮亞季的呼喚》更新後的版本(可能是 Open X-Ray 的分支),所以過去那些將遊戲內容解包到根目錄「gamedata」資料夾的 MOD 修改方式依然適用。要從遊戲的 .db 檔案中提取所有內容,你需要下面這個工具:
db_unpacker.zip 下載連結:
https://pixeldrain.com/u/GX9RtCFU
這個解包工具是為 SoCEE 客製化的,請不要拿它去解包《晴空》或《普里皮亞季的呼喚》的強化版喔!
解包步驟教學
- 下載上方的壓縮檔,並將其解壓縮到你的遊戲根目錄(例如:D:\SteamLibrary\steamapps\common\STALKER Shadow of Chornobyl - EE)。
- 啟動遊戲,進入主選單。
- 按下 F1 鍵。
- 按下 鍵(波浪號鍵)打開控制台。
- 再次按下 鍵關閉控制台。
- 按下 A 鍵開始提取所有資料。
在提取過程中,遊戲會凍結,請耐心等待大約 2-3 分鐘(使用 NVMe M.2 SSD 的情況下)。完成後,你就可以在遊戲主資料夾看到一個名為「unpack」的資料夾。這個過程需要 11.2 GB 的磁碟空間。
解包完成後,為了避免後續製作作弊選單時發生混淆,你可以先刪除「gamedata」資料夾裡的「config」和「scripts」這兩個子資料夾。
我需要這個解包工具,才能將「願望實現器」(也就是作弊選單)整合進遊戲裡。你可以參考下方影片 3:14 處展示的概念:
外連至此YOUTUBE影片連結
我花了一些時間才搞懂並重新設計了適用於強化版的「願望實現器」。這篇教學會一步步教你如何做到。這並不像把舊版遊戲檔案複製貼上到新版那麼簡單,而且我也想學習如何親手完成它。
簡單來說,我重複利用了「載入遊戲」的對話框和其屬性檔案(.script, .xml),並對其內容進行了修改和添加。過程中,我還需要用像 Adobe Fireworks 這樣的繪圖工具來做視覺規劃,才能確定所有介面元素的位置(X、Y、寬、高)。
為了方便跟隨本教學,建議你先將遊戲的解析度調到最低(例如 1176x664),並將顯示模式設定為「視窗化」。本教學需要一些基本的 Lua 和 XML 知識,你不僅僅是盲目地跟著步驟走,而是需要理解背後的原理。
第二步:前置準備工作
首先,我們需要準備好要修改的基礎檔案。請從剛剛解包出來的「unpack」資料夾中,複製以下 3 個檔案到遊戲根目錄下的「gamedata」對應路徑中。
如果你的遊戲根目錄沒有「gamedata」資料夾,請手動建立一個。
複製來源:
- game_root\unpack\scripts\ui_main_menu.script
- game_root\unpack\scripts\ui_load_dialog.script
- game_root\unpack\config\ui\ui_mm_load_dlg.xml
複製到目標位置:
- game_root\gamedata\scripts\ui_main_menu.script
- game_root\gamedata\scripts\ui_load_dialog.script
- game_root\gamedata\config\ui\ui_mm_load_dlg.xml
(請注意,game_root 是指你的遊戲安裝路徑,每個人的可能都不同。)
👉 GM後台版 遊戲 推薦 ⬇️⬇️⬇️ 快速玩各種二次元動漫手遊app

為了方便區分,我將 ui_mm_load_dlg.xml 和 ui_load_dialog.script 分別重新命名為 ui_mm_cheat_dlg.xml 和 ui_cheat_dialog.script。
完成後,你的 gamedata 資料夾結構應該如下圖所示(請使用你自己的遊戲路徑):
D:\SteamLibrary\steamapps\common\STALKER Shadow of Chornobyl - EE\gamedata\scripts\ui_cheat_dialog.script
D:\SteamLibrary\steamapps\common\STALKER Shadow of Chornobyl - EE\gamedata\scripts\ui_main_menu.script
D:\SteamLibrary\steamapps\common\STALKER Shadow of Chornobyl - EE\gamedata\config\ui\ui_mm_cheat_dlg.xml
第三步:設定觸發方式
為了讓遊戲讀取我們新建的 ui_cheat_dialog.script 和 ui_mm_cheat_dlg.xml,我們需要修改主選單的腳本 ui_main_menu.script。所有 .script 檔案都是 Lua 腳本,你可以使用 Notepad++ 這類編輯器來開啟它。
- 打開位於 game_root\gamedata\scripts 資料夾的 ui_main_menu.script。
- 找到 main_menu:OnKeyboard 這個函式。遊戲原本內建一個生成對話框的功能,但在正式版中被移除了。我們正好可以利用它來載入我們的作弊選單。
- 將被註解掉的程式碼恢復。移除 --[[ 和 ]]-- 符號,讓它看起來像這樣:
- 接著,把觸發按鍵從 DIK_S 改成 DIK_F1。這樣一來,F1 鍵就會成為我們的觸發熱鍵。
- 現在 F1 鍵會執行 OnButton_load_spawn 這個函式。從程式碼中可以看出,它會初始化一個名為「ui_spawn_dialog」的對話框。
- 現在儲存檔案並執行遊戲。在主選單按下 F1,你會看到一個沒什麼用的空白對話框,但這代表我們成功觸發了它!
- 滿足好奇心後,讓我們把它改成載入我們的作弊選單。回到 ui_main_menu.script,將 main_menu:OnButton_load_spawn 函式修改成以下內容:
- function main_menu:OnButton_load_spawn()
- if self.cheat_dlg == nil then
- self.cheat_dlg = ui_cheat_dialog.cheat_dialog()
- self.cheat_dlg.owner = self
- end
- self.cheat_dlg:FillList()
- self:GetHolder():start_stop_menu(self.cheat_dlg, true)
- self:GetHolder():start_stop_menu(self, true) --new
- self:Show(false)
- end
複製代碼 - 儲存 ui_main_menu.script。然後打開 ui_cheat_dialog.script,用 Ctrl+F 尋找所有的 load_dialog 並替換為 cheat_dialog。接著在約 108 行處,將 xml:ParseFile ("ui_mm_load_dlg.xml") 改成 xml:ParseFile ("ui_mm_cheat_dlg.xml")。你也可以刪除 109 和 110 行,我們用不到。修改後如下圖:
- 儲存檔案並重新執行遊戲。現在在主選單按下 F1,你將會看到原本的「載入遊戲」選單出現了!這表示我們已經成功地用 F1 鍵載入了我們修改過的腳本檔案,接下來就是把它改造成「願望實現器」了。
第四步:簡化程式碼,重新開始
為了更容易理解和建構,我們將從簡化的程式碼開始,而不是修改現有的複雜內容。
簡化 Lua 腳本
請將 ui_cheat_dialog.script 檔案的內容完全替換為以下程式碼。我已經將暫時用不到的部分註解掉了。- local mode
- local s_table = {}
- class "cheat_item" (CUIListBoxItem)
- function cheat_item:__init(height) super(height)
- self.file_name = "filename"
- self:SetTextColor(GetARGB(255, 170, 170, 170))
- self.fn = self:GetTextItem()
- self.fn:SetFont(GetFontLetterica18Russian())
- self.fn:SetEllipsis(true)
- end
- function cheat_item:__finalize()
- end
- class "cheat_dialog" (CUIScriptWnd)
- function cheat_dialog:__init() super()
- self:InitControls()
- self:InitCallBacks()
- end
- function cheat_dialog:__finalize()
- end
- function cheat_dialog:FillList()
- self.list_box:RemoveAll()
- mode = "item"
- local name
- for i, v in ipairs(cheat_tables.food_and_drugs) do
- name = game.translate_string(system_ini():r_string(v, "inv_name"))
- self:AddItemToList(name, v)
- end
- end
- function cheat_dialog:InitControls()
- self:Init(0, 0, 1024, 768)
- local xml = CScriptXmlInit()
- local ctrl
- xml:ParseFile("ui_mm_cheat_dlg.xml")
- local platform = get_platform_id()
- if is_using_4k_movies() then
- xml:InitStatic("back_video_4k", self)
- elseif platform == platform_ids.PLATFORM_ORBIS or platform == platform_ids.PLATFORM_PROSPERO or platform == platform_ids.PLATFORM_GDK then
- xml:InitStatic("back_video_orbis", self)
- elseif platform == platform_ids.PLATFORM_GDK_1440 then
- xml:InitStatic("back_video_orbis", self)
- elseif platform == platform_ids.PLATFORM_GDK_4K then
- xml:InitStatic("back_video_orbis", self)
- elseif platform == platform_ids.PLATFORM_NX64 then
- xml:InitStatic("back_video_nx64", self)
- else
- xml:InitStatic("back_video", self)
- xml:InitStatic("background", self)
- xml:InitStatic("newspaper_video", self)
- end
- ctrl = CUIWindow()
- xml:InitWindow("file_item:main", 0, ctrl)
- self.file_item_main_sz = vector2():set(ctrl:GetWidth(), ctrl:GetHeight())
- xml:InitWindow("file_item:fn", 0, ctrl)
- self.file_item_fn_sz = vector2():set(ctrl:GetWidth(), ctrl:GetHeight())
- xml:InitWindow("file_item:fd", 0, ctrl)
- self.file_item_fd_sz = vector2():set(ctrl:GetWidth(), ctrl:GetHeight())
- self.form = xml:InitStatic("form", self)
- xml:InitStatic("form:caption", self.form)
- self.file_caption = xml:InitStatic("form:file_caption", self.form)
- self.file_data = xml:InitStatic("form:file_data", self.form)
- xml:InitFrame("form:list_frame", self.form)
- self.list_box = xml:InitListBox("form:list", self.form)
- self.list_box:ShowSelectedItem(true)
- self:Register(self.list_box, "list_window")
- if not self.mm_is_controller or get_platform_id() == platform_ids.PLATFORM_NX64 then
- self.load_btn = xml:Init3tButton("form:btn_load", self.form)
- self:Register(self.load_btn, "button_load")
- self.delete_btn = xml:Init3tButton("form:btn_delete", self.form)
- self:Register(self.delete_btn, "button_del")
- self.cancel_btn = xml:Init3tButton("form:btn_cancel", self.form)
- self:Register(self.cancel_btn, "button_back")
- if get_platform_id() == platform_ids.PLATFORM_NX64 then
- self.input_legend = xml:InitInputLegend("input_legend", self)
- end
- else
- self.input_legend = xml:InitInputLegend("input_legend", self)
- end
- --[[ Omitted Code ]]
- self.message_box = CUIMessageBoxEx()
- self:Register(self.message_box, "message_box")
- end
- function cheat_dialog:InitCallBacks()
- self:AddCallback("message_box", ui_events.MESSAGE_BOX_YES_CLICKED, self.OnMsgYes, self)
- self:AddCallback("message_box", ui_events.MESSAGE_BOX_OK_CLICKED, self.OnMsgYes, self)
- self:AddCallback("message_box", ui_events.MESSAGE_BOX_NO_CLICKED, self.OnMsgNo, self)
- self:AddCallback("message_box", ui_events.MESSAGE_BOX_CANCEL_CLICKED, self.OnMsgNo, self)
- if not self.mm_is_controller or get_platform_id() == platform_ids.PLATFORM_NX64 then
- self:AddCallback("button_load", ui_events.BUTTON_CLICKED, self.OnButton_load_clicked, self)
- self:AddCallback("button_back", ui_events.BUTTON_CLICKED, self.OnButton_back_clicked, self)
- self:AddCallback("button_del", ui_events.BUTTON_CLICKED, self.OnButton_del_clicked, self)
- self:AddCallback("list_window", ui_events.LIST_ITEM_CLICKED, self.OnListItemClicked, self)
- self:AddCallback("list_window", ui_events.WINDOW_LBUTTON_DB_CLICK, self.OnListItemDbClicked, self)
- if get_platform_id() == platform_ids.PLATFORM_NX64 then
- self:AddCallback("list_window", ui_events.LIST_ITEM_SELECT, self.OnListItemClicked, self)
- end
- else
- self:AddCallback("list_window", ui_events.LIST_ITEM_SELECT, self.OnListItemClicked, self)
- end
- --[[ Omitted Code ]]
- IGNORE_WHEN_COPYING_START
- content_copy
- download
- Use code with caution.
- IGNORE_WHEN_COPYING_END
- end
- function cheat_dialog:OnButton_back_clicked()
- if self.mm_is_controller then
- self.sndDecline:play(nil, 0.0, sound_object.s2d)
- end
- self:GetHolder():start_stop_menu(self.owner, true)
- self:GetHolder():start_stop_menu(self,true)
- self.owner:Show(true)
- end
複製代碼 接著,打開 ui_main_menu.script,找到 self.cheat_dlg:FillList() 這行(大約在 364 行),在前面加上 -- 將它暫時註解掉。
簡化 XML 介面檔案
同樣地,我們也從一個乾淨的 XML 檔案開始。請將 ui_mm_cheat_dlg.xml 的內容完全替換為以下程式碼:- <?xml version="1.0" encoding="utf-8"?>
- <window>
- <!-- 根據影像模式載入背景圖片 -->
- <back_video_orbis x="0" y="0" width="1024" height="768" stretch="1">
- <texture x="0" y="0" width="1920" height="1080">ui\ui_mm_load_back_new</texture>
- </back_video_orbis>
- <back_video_4k x="0" y="0" width="1024" height="768" stretch="1">
- <texture x="0" y="0" width="3840" height="2160">ui\ui_mm_load_back_new</texture>
- </back_video_4k>
- <back_video x="0" y="0" width="1024" height="430">
- <texture x="0" y="0" width="1024" height="430">ui\ui_mm_window_back_crop</texture>
- </back_video>
- <!-- 重複使用一個影片檔來填補黑色背景空隙 -->
- <_back_video x="0" y="0" width="1024" height="512" stretch="1">
- <texture>ui\ui_vid_back_04</texture>
- </_back_video>
- <!-- 背景圖 -->
- <background x="0" y="0" width="1024" height="768">
- <texture ng_ratio="2">ui\ui_static_mm_back_04</texture>
- </background>
- <!-- 列表項目的名稱和描述欄位的寬高設定 -->
- <file_item>
- <main width="392" height="18"/>
- <!-- name -->
- <fn width="284" height="18"/>
- <!-- description -->
- <fd width="88" height="18"/>
- </file_item>
- <!-- 主視窗本體 -->
- <form x="415" y="168" width="560" height="460">
- <!-- 對話框皮膚 -->
- <_texture>ui\ui_options_menu_static</_texture>
- <_texture_offset x="-29" y="-19"/>
- <texture>ui_menu_options_dlg</texture>
-
- <!-- 標題與屬性:位置xy, 大小wh, 標題文字 -->
- <caption x="65" y="10" width="500" height="25" complex_mode="0">
- <text font="graffiti32">The Wish Granter</text>
- </caption>
-
- </form>
- IGNORE_WHEN_COPYING_START
- content_copy
- download
- Use code with caution.
- IGNORE_WHEN_COPYING_END
- </window>
複製代碼第五步:介面詳解與調整
遊戲介面的預設畫布大小是 1024x768,所有的介面物件都以此為基準進行定位。而我們的主視窗(form)屬性為- <form x="415" y="168" width="560" height="460">
複製代碼 ,這代表它是一個位於座標 (415, 168),寬高為 560x460 的矩形。
file_item 區塊定義了列表中每一行的格式,包含一個寬 284 像素的名稱欄(fn)和一個寬 88 像素的描述欄(fd)。
form 區塊則定義了主視窗的外觀和標題。我們將標題文字改為「The Wish Granter」,並使用更大的「graffiti32」字體。
執行遊戲後,你會看到如下畫面。標題位置有點偏,而且沒有按鈕可以返回主選單,你只能強制關閉遊戲。
現在,讓我們來修正這個問題。在 XML 檔案的 </form> 標籤前,加入以下按鈕的程式碼:- <!-- buttons -->
- <btn_load x="65" y="427" width="157" height="48">
- <texture>ui_button_main01</texture>
- <text font="graffiti22">Apply</text>
- </btn_load>
- <btn_delete x="221" y="427" width="157" height="48">
- <texture>ui_button_main01</texture>
- <text font="graffiti22">Stop (Music)</text>
- </btn_delete>
- <btn_cancel x="377" y="427" width="157" height="48">
- <texture>ui_button_main01</texture>
- <text font="graffiti22">Cancel</text>
- </btn_cancel>
複製代碼 現在介面看起來有模有樣了。點擊「Cancel」按鈕,就可以順利返回主選單。
最後,我們微調一下標題的位置,讓它看起來更美觀。修改 <caption> 標籤的 x 和 y 值:- <!-- caption and properties: position xy, size wh, title -->
- <caption x="45" y="2" width="500" height="25" complex_mode="0">
- <text font="graffiti32">The Wish Granter</text>
- </caption>
複製代碼 大功告成!現在你的介面看起來應該像這樣:
這篇教學會隨著我的進度持續更新,敬請期待!
參考資料
db_unpacker 來源討論串
https://ap-pro.ru/forums/topic/6 ... ents#comment-337523
db_unpacker 作者原始連結
https://gitlab.com/axet/gist/-/tree/extractor-op2.1
以下廣告滑動後還有帖子內容
《浩劫殺陣:車諾比之影》自製選單常見問題Q&A
Q:為什麼我執行解包步驟時,遊戲會卡住或凍結?
A:這是正常現象。解包過程需要讀取大量遊戲資料,會暫時讓遊戲無回應。請耐心等待 2-3 分鐘,具體時間取決於你的硬碟讀寫速度。
Q:這個解包工具可以用在《晴空》或《普里皮亞季的呼喚》強化版嗎?
A:不行。本教學提供的解包工具是專為《車諾比之影》強化版客製化的,請勿混用。
Q:我的遊戲根目錄下沒有「gamedata」資料夾,該怎麼辦?
A:請手動在遊戲根目錄下建立一個名為「gamedata」的資料夾即可。
Q:我照著步驟做了,但按 F1 遊戲就閃退,是哪裡出錯了?
A:最可能的原因是 .script 或 .xml 檔案的程式碼有誤,例如打錯字、複製貼上時漏掉內容,或是檔案名稱、路徑不正確。請仔細核對教學中的每一步,特別是檔案名稱和程式碼內容。
Q:我可以把觸發熱鍵 F1 改成別的按鍵嗎?
A:可以。在「第三步:設定觸發方式」中,修改 ui_main_menu.script 檔案時,可以將 DIK_F1 改成你想要的按鍵碼,例如 DIK_F2 就是 F2 鍵。
|