[ 6-Jun-2025 ]
Took me a bit to understand and re-design the "Wish Granter" (as seen in CoP), so here goes. Initially I just wanted it to be a simple mod release, but fuck it. Here's a tutorial (haven't seen any talks about it online). And yeah, it's not as simple as copy-pasting files from the old version of the game to the EE one, plus I wanted to learn how to do it myself. So:
- I've re-used the Load game dialog and properties (.script, .xml) and adjusted the content, editing or adding to it.
- I had to do a visual representation in a graphical tool like Adobe Fireworks to get an idea where to place all the items I wanted (X,Y,W,H).
- You will have it attached here, like a guide, as well as comments of all the XML properties along the way, in the .xml file.
- I recommend setting the game to the lowest resolution (in my case, 1176x664) and setting Display mode to Windowed, if you wanna follow this guide
- Some of the actions require a modicum of knowledge, so excuse me if I don't explain things like to a baby or hold your hand fully through the process. You're expected to understand what Lua and XML are, not just blindly follow spoon-fed steps.
- Assets used for this:
Code: Select all
- 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 = D:\SteamLibrary\steamapps\common\STALKER Shadow of Chornobyl - EE (in my case; it's the installation folder)
------------------------------------------------------------------------------------------------------------------------------------------------
Step 1: Requisites
------------------------------------------------------------------------------------------------------------------------------------------------
Copy the 3 files mentioned above to "game_root\gamedata\scripts" and "game_root\gamedata\config" folders (where "game_root" in my case is "D:\SteamLibrary\steamapps\common\STALKER Shadow of Chornobyl - EE"):
Recap -- copy:
Code: Select all
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
to
Code: Select all
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
If you haven't yet created the "gamedata" folder in root, create it by hand (for those who will go "but I don't have a gamedata folder"...).
NOTES:
1) Your game folder might be in another location, so don't blindly follow what you see in the pictures. My installation path is "D:\SteamLibrary\steamapps\common\STALKER Shadow of Chornobyl - EE", yours will probably be different, is all I'm saying.
2) I've renamed the "ui_mm_load_dlg.xml" and "ui_load_dialog.script" to "ui_mm_cheat_dlg.xml" and "ui_cheat_dialog.script", respectively.
3) Ignore any other file you see in the screenshots for now.
So your hierarchy should look like this now (use your own "game_root"):
Code: Select all
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
------------------------------------------------------------------------------------------------------------------------------------------------
Step 2: Trigger
------------------------------------------------------------------------------------------------------------------------------------------------
In order for the game to load your "ui_cheat_dialog.script" and "ui_mm_cheat_dlg.xml", we need to modify a routine in "ui_main_menu.script". I will mention now that all ".script" files are
Lua scripts; just so it's clear. So open up "ui_main_menu.script" (from game_root\gamedata\scripts\ folder) in
Notepad++, set language to Lua from the top menu and you should see this:
The game used to have a spawn dialog where you could do a bunch of things, but obviously, this is removed in retail. So let's use it for ourselves, replacing what used to be loaded with our future Wish Granter

So, in "main_menu:OnKeyboard" function, you will see this:
Uncomment the lines (removing the --[[ and ]]-- symbols) so it looks like this:
Also change "DIK_S" to "DIK_F1". This way,
F1 key will be our trigger key, instead of
S:
What does F1 key do now? Well, it runs a function called "OnButton_load_spawn". If you search for it in this file, you will find it a bit further up:
From reading the code, it initializes/grabs Mod data and spawns a dialog called "ui_spawn_dialog" via "spawn_dialog()" function. You can leave it as is for now and save "ui_main_menu.script" file. The order of loading assets is first from game files (the .db archives), then from "gamedata" folder. That's why we need this folder and to maintain the hierarchy. So now run the game, press F1 at main menu and see what happens. And this is what happens:
Like I said, nothing of use in there. But hey, you got the game to open a dialog page/form
Having satisfied your curiosity, let's now change this routine to load our "ui_cheat_dialog.script" / "ui_mm_cheat_dlg.xml". With "ui_main_menu.script" still open in Notepad++, we change "main_menu:OnButton_load_spawn" function to look like this:
Code: Select all
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
Why like this? "cheat_dlg" is just a variable name, but I wanted to be consistent. Could've kept it as "load_dlg". However, "ui_load_dialog" has to be changed to the name of the .script file; which, in our case, it's "ui_cheat_dialog"(.script) now. Then, inside this latter file, we'll soon make some adjustments so the main function will be "cheat_dialog".
Save ui_main_menu.script.
Then open "ui_cheat_dialog.script" in Notepad++. In here, after setting language to Lua, Ctrl+F for "load_dialog" and change it to "cheat_dialog". Then on line 108 you will see " xml:ParseFile ("ui_mm_load_dlg.xml")". Change "ui_mm_load_dlg" to "ui_mm_cheat_dlg". You can remove lines 109 and 110, we'll not use those. This is what it will look like after the edits:
Save file and re-run game. Press F1 at main menu and this is what will happen:
We've now loaded the "Load game" menu, but using F1 key, which loads content from our modified .script file name to distinguish it from the original assets. And the .script file parses our .xml file instead of the original one. These initial edits will now represent the basis for our transformation of this dialog/menu into the "Wish Granter".
------------------------------------------------------------------------------------------------------------------------------------------------
Step 3: Starting Up
------------------------------------------------------------------------------------------------------------------------------------------------
For the transformation to happen, you will need to understand both Lua code and XML. They work hand in hand, XML for the design, Lua for the display/render and menu functionality. I'll try to explain stuff to the best of my abilities, but I might not be able to sync the two parts properly for the user's super clear understanding (hence why I said you should have XML and Lua knowledege). So if you have questions or don't get something, shoot a question.
I started the transformation by simplifying the Lua code to the bare minimum, so we can build upon it, rather than reuse everything that's in the .script file. So my "ui_cheat_dialog.script" file now looks like this:
Code: Select all
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:SetWindowName("filelistboxload")
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
--[[
ctrl = xml:Init3tButton("form:btn_load", self.form)
self:Register(ctrl, "button_load")
ctrl = xml:Init3tButton("form:btn_delete", self.form)
self:Register(ctrl, "button_del")
self.wt_but = ctrl
ctrl = xml:Init3tButton("form:btn_cancel", self.form)
self:Register(ctrl, "button_back")
self.message_box = CUIMessageBoxEx()
self:Register (self.message_box,"message_box")
self.cap_rubel = xml:InitStatic("form:cap_rubel", self.form)
self.cap_spawn = xml:InitStatic("form:cap_spawn", self.form)
self.cap_loc = xml:InitStatic("form:cap_loc", self.form)
self.spin_rubel = xml:InitSpinNum("form:spin_rubel", self.form)
self.spin_rubel:Show (true)
self.cap_rubel_coeff = xml:InitStatic("form:cap_rubel_coeff", self.form)
self.cap_rubel_coeff:TextControl():SetText (game.translate_string("x") .. " " .. tostring(RU_coeff) .. " RU")
self.cap_rubel_currently = xml:InitStatic("form:cap_rubel_currently", self.form)
if db.actor ~= nil then actor_money = db.actor:money() else actor_money = 0 end
self:RefreshMoneyDisplay()
btn = xml:Init3tButton("form:btn_moneyplus", self.form)
self:Register (btn, "button_moneyplus")
btn = xml:Init3tButton("form:btn_moneyminus", self.form)
self:Register (btn, "button_moneyminus")
self.cap_timefactor = xml:InitStatic("form:cap_timefactor", self.form)
self.cap_timefactor_currently = xml:InitStatic("form:cap_timefactor_currently", self.form)
if level.present() then time_factor = level.get_time_factor() else time_factor = 10 end
self.cap_timefactor_desc = xml:InitStatic("form:cap_timefactor_desc", self.form)
self:RefreshTimeFactorDisplay()
btn = xml:Init3tButton("form:btn_timeplus", self.form)
self:Register (btn, "button_timeplus")
btn = xml:Init3tButton("form:btn_timeminus", self.form)
self:Register (btn, "button_timeminus")
self.spin_spawn = xml:InitSpinNum("form:spin_spawn", self.form)
self.spin_spawn:Show (true)
btn = xml:Init3tButton("form:btn_surge", self.form)
self:Register (btn, "button_surge")
btn = xml:InitCheck("form:check_wt", self)
self:Register (btn, "check_wt")
self.chek_weather = btn
if db.actor then
self.chek_weather:SetCheck(god.load_var("weather_state",false))
end
self.combo_renderer = xml:InitComboBox("form:list_renderer", self)
self:Register (self.combo_renderer, "combo_renderer")
self.combo_renderer:AddItem ("1. Weather", 1)
self.combo_renderer:AddItem ("2. Teleport (Fast Travel Dialog)", 2)
self.combo_renderer:AddItem ("3. Stalkers", 3)
self.combo_renderer:AddItem ("4. Mutants", 4)
self.combo_renderer:AddItem ("5. Ammunition", 5)
self.combo_renderer:AddItem ("6. Weapons", 6)
self.combo_renderer:AddItem ("7. Med/Devices/Food", 7)
self.combo_renderer:AddItem ("8. Artifacts", 8)
self.combo_renderer:AddItem ("9. Outfits/Armour", 9)
self.combo_renderer:AddItem ("10. Quest Items", 10)
self.combo_renderer:AddItem ("11. Anomalies", 11)
self.combo_renderer:AddItem ("12. World Items", 12)
self.combo_renderer:AddItem ("13. Join Factions", 13)
self.combo_renderer:AddItem ("14. Videos", 14)
self.combo_renderer:AddItem ("15. Music", 15)
self.combo_renderer:AddItem ("16. Squads", 16)
self.combo_renderer:AddItem ("17. Teleport Coordinates", 17)
-- ћеню телепортации --
self.dialog = xml:InitStatic("dialog", self)
xml:InitStatic ("dialog:capt", self.dialog)
xml:InitStatic ("dialog:msg2", self.dialog)
xml:InitStatic ("dialog:msg3", self.dialog)
self.dialog:Show(false)
-- кнопки
self:Register(xml:Init3tButton("dialog:btn_1", self.dialog),"btn_1")
self:Register(xml:Init3tButton("dialog:btn_2", self.dialog),"btn_2")
self:Register(xml:Init3tButton("dialog:btn_3", self.dialog),"btn_3")
self:Register(xml:Init3tButton("dialog:btn_4", self.dialog),"btn_4")
self:Register(xml:Init3tButton("dialog:btn_5", self.dialog),"btn_5")
btn = xml:InitEditBox("dialog:edit_box", self.dialog)
self.edit_box = btn
self:Register(btn, "edit_box")
btn = xml:InitEditBox("dialog:edit_box2", self.dialog)
self.edit_box2 = btn
self:Register(btn, "edit_box2")
btn = xml:InitEditBox("dialog:edit_box3", self.dialog)
self.edit_box3 = btn
self:Register(btn, "edit_box3")
btn = xml:InitEditBox("dialog:edit_box4", self.dialog)
self.edit_box4 = btn
self:Register(btn, "edit_box4")
]]
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
--[[
self:AddCallback("button_moneyplus", ui_events.BUTTON_CLICKED, self.OnButton_moneyplus_clicked, self)
self:AddCallback("button_moneyminus", ui_events.BUTTON_CLICKED, self.OnButton_moneyminus_clicked, self)
self:AddCallback("button_timeplus", ui_events.BUTTON_CLICKED, self.OnButton_timeplus_clicked, self)
self:AddCallback("button_timeminus", ui_events.BUTTON_CLICKED, self.OnButton_timeminus_clicked, self)
self:AddCallback("button_spawnitem", ui_events.BUTTON_CLICKED, self.OnButton_spawnitem_clicked, self)
self:AddCallback("combo_renderer", ui_events.LIST_ITEM_SELECT, self.ModeChanges, self)
self:AddCallback("btn_1", ui_events.BUTTON_CLICKED, self.OnButton_btn1_clicked, self)
self:AddCallback("btn_2", ui_events.BUTTON_CLICKED, self.OnButton_btn2_clicked, self)
self:AddCallback("btn_3", ui_events.BUTTON_CLICKED, self.OnButton_btn3_clicked, self)
self:AddCallback("btn_4", ui_events.BUTTON_CLICKED, self.OnButton_btn4_clicked, self)
self:AddCallback("btn_5", ui_events.BUTTON_CLICKED, self.OnButton_btn5_clicked, self)
self:AddCallback("button_surge", ui_events.BUTTON_CLICKED, self.OnButton_surge_clicked, self)
]]
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
You may copy the above and replace the content of "ui_cheat_dialog.script" file with it.
Where you see --[[ ... ]], those are commented out, as I have no use for those snippets for the moment.
Explanation of the functions:
- cheat_item -- class for the list-box we will populate with menu items
- cheat_dialog -- class for the main dialog-box which will operate Lua functions
- each class has an __init and __finalize function (see them as constructors/destructors)
- FillList() -- takes care of filling lists
- InitControls() -- handles the initialization of the dialog-box, which will overlap with what is parsed from the XML; basically bring Lua interaction to XML objects
- OnButton_back_clicked -- self-explanatory
One more thing we have to do before we begin is to comment out "self.cheat_dlg:FillList()" in "ui_main_menu.script" for the time being. So open that file, head to line 364 (or search for it) and put a -- in front of the text to comment the Lua code out.
Similarly, I've removed everything from the .xml file, just to start fresh with the things I understood from it. Again, we'll build upon it rather than keeping everything and modifying here and there. So here's my initial "" file:
Code: Select all
<?xml version="1.0" encoding="utf-8"?>
<window>
<!-- load the background image based on video mode: ui_mm_load_back_new.dds, ui_mm_window_back_crop.ogm -->
<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>
<!-- reuse one of the video files to cover the black background gaps: ui_vid_back_04.ogm -->
<_back_video x="0" y="0" width="1024" height="512" stretch="1">
<texture>ui\ui_vid_back_04</texture>
</_back_video>
<!-- for the background, use the ui_static_mm_back_04.dds file -->
<background x="0" y="0" width="1024" height="768">
<texture ng_ratio="2">ui\ui_static_mm_back_04</texture>
</background>
<!-- width and height of the 2 columns table listing items and names -->
<file_item>
<main width="392" height="18"/>
<!-- name -->
<fn width="284" height="18"/>
<!-- description -->
<fd width="88" height="18"/>
</file_item>
<!-- the actual form box -->
<form x="415" y="168" width="560" height="460">
<!-- the dialog box form: ui_options_menu_static.dds -->
<_texture>ui\ui_options_menu_static</_texture>
<_texture_offset x="-29" y="-19"/>
<texture>ui_menu_options_dlg</texture>
<!-- caption and properties: position xy, size wh, title -->
<caption x="65" y="10" width="500" height="25" complex_mode="0">
<text font="graffiti32">The Wish Granter</text>
</caption>
</form>
</window>
You may copy the above and replace the content of "ui_mm_cheat_dlg.xml" file with it.
Where you see <!-- ... -->, those are comments so you better understand what the content BELOW the comment refers to.
------------------------------------------------------------------------------------------------------------------------------------------------
Step 4: Explanation/Adjustments
------------------------------------------------------------------------------------------------------------------------------------------------
The interface itself is considered to start at the default value of 1024x768 (see .\SteamLibrary\steamapps\common\STALKER Shadow of Chornobyl - EE\unpack\config\ui\ui_mm_load_dlg.xml file):
Code: Select all
<background x="0" y="0" width="1024" height="768">
That is the reference used to offset all parent objects in the menu. The elements are either stretched (stretch=1) or a ratio applied (ng_ratio=2) to enlarge them, so they cover the resolution size you've picked in the
Options menu. The "Load game" form/dialog box has these properties:
Code: Select all
<form x="415" y="168" width="560" height="460">
So I started from this in Fireworks, creating an 1024x768 canvas, where I then added a rectangle of WxH = 560x460, at {X;Y} = {415;168} position:
Looking at the XML file, I skipped the "newspaper_video" part, as we really don't need it for our goal - The Wish Granter menu. The "file_item" represents a table of 2 columns where [n]ame and [d]escription will be displayed ("fn" and "fd"):
Code: Select all
<!-- width and height of the 2 columns table listing items and names -->
<file_item>
<main width="392" height="18"/>
<!-- name -->
<fn width="284" height="18"/>
<!-- description -->
<fd width="88" height="18"/>
</file_item>
So, in the picture above, "file_item" is 1 row of that table, of 18 pixels in height, where "fn" is the first column, having a width of 284 pixels and "fd" is the second column, with a width of 88 pixels. The table has 2 rows.
Looking now at the "form" section in the XML:
Code: Select all
<!-- the actual form box -->
<form x="415" y="168" width="560" height="460">
<!-- the dialog box form: ui_options_menu_static.dds -->
<_texture>ui\ui_options_menu_static</_texture>
<_texture_offset x="-29" y="-19"/>
<texture>ui_menu_options_dlg</texture>
<!-- caption and properties: position xy, size wh, title -->
<caption x="65" y="10" width="500" height="25" complex_mode="0">
<text font="graffiti32">The Wish Granter</text>
</caption>
- the "texture" used to draw the form is stored in "ui_options_menu_static.dds" file; this will represent the "skin" applied to the form
- the "caption" is self explanatory, you being able to control xy, wh, font and color; we'll replace this with "The Wish Granter" (and leave it in striking white color, so removing the "r", "g" and "b" properties) and change font to "graffiti32", so it's larger
- note that xy properties are offsetted from the "form" (parent) and not the whole 1024x768 screen size
So up until the above, it will look like this:
As you can see, the text caption doesn't fit quite right in the top-left of the skinned dialog and also we have no buttons to return to main menu. So you'll have to kill the game from the top-right [X] button to re-test. We already have the Lua code that controls the buttons, but the buttons aren't yet displayed because there's no associated XML elements for them.
Let's fix this.
Add the following to the XML file:
Code: Select all
<!-- 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>
So we have this now:
With Lua code in the .script file we will change what these buttons do when clicked on. Why those labels on the buttons: that's how they were called in the "Wish Granter" in CoP or True Stalker (see:
viewtopic.php?t=35304). My intention is to obtain the same crap, but in SoCEE

Not reinvent the wheel.
So now when you click "Cancel", you will be taken back to main menu. The button's code is in "ui_cheat_dialog.script" file:
Code: Select all
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
Also, let's fix that caption text position in the .xml (changing the "x" and "y" properties):
Code: Select all
<!-- 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>
So now it looks much better:
------------------------------------------------------------------------------------------------------------------------------------------------
[ 12-Jun-2025 ]
While building this out I stumbled into a series of aspects that makes porting of content not that simple. For starters, the developers decided to make some changes to the core framework, UI-related, because they thought it makes no sense to keep in functions related to combo boxes, especially when those combo boxes (drop-downs) are solely in the menu options (e.g.: when you change display mode or select resolution). As such, creating a combo box wasn't the issue, but
populating it was a hurdle. I wanted to create something like this:
You can see the top label "Categories:" and the drop-down. Like I said, populating it with selection items doesn't work like in the original game, because:
Code: Select all
self.cmb_categories = xml:InitComboBox("form:cmb_categories", self)
self.cmb_categories:AddItem("1. Weather", 1) <-- crash
So obtaining something like this isn't possible this way:
In the meantime, going through all the STALKER game Complete versions on moddb.com, I found that the maker of this similar dialog/menu in
[Link] had the idea of moving the drop-down items to
buttons. That being said, I've tracked him down on moddb.com and
asked for his approval to use this idea in what I will create for SoC - EE. The menu in Clear Sky Complete looks like this:
And this is the request for approval and permission granted:
Reason I am posting the above is for those people (you know who you are) who can't wait to shove it in my face that I didn't ask for permission out of spite or hate.
------------------------------------------------------------------------------------------------------------------------------------------------
[ 13-Jun-2025 ]
While starting with the above, I've re-enabled the versioning at the bottom of main menu:
Problem is.. the "1.8.0" string is static. The
mm:GetGSVer() function doesn't return a string anymore, hence why it's commented out in "ui_main_menu.script". The game version isn't present in any data files that Lua could easily read, but hey.. CHALLENGE. Yeah, I know, why the hell pursue this if it really isn't that useful? Cuz I wanted to achieve it
So I thought of doing it like the below, after some research:
- using Lua to obtain information that you would get with GetFileVersionInformation API isn't possible
- this information is basically metadata that is stored within the binary file (xrEngine.exe)
- to get to this information, you would need to: open the file, read from it, parse that content, extract whatever information you need, use it
So how to do this? If we look at the exe:
We will open the file in game's Lua, read it in
binary mode (either all of it -OR- in chunks of 4KB) and use patterns/signatures to extract that "1.8.0" from either the "File version" or "Product version". Cool.. In Windows Properties window it looks nice and dandy, but how is that stored in the file itself? Let's use a hex editor (I use
HxD) and search for the string:
What can we tell from the picture above? We can levarage some strings before or after the actual version string ("boundary=", "platform", "version.basic", "version.complete"). In-between these we can extract either "1.8.0" or "1.8.0+36-3881299" (which we can parse to extract just 1.8.0 out of it). Another thing we notice is this location is neither at the start of the file, nor at the end of it, but somewhat in the middle. Meaning we can't use optimization tricks to read directly from a specific offset/file pointer.
So the logic now is this:
- read data size == file size (we'll use this later on, you'll see how)
- parse content to find those signature strings and get the version
- store it in an external file, along with the file size
- in any subsequent runs, check if this file exists and read from it; if not, run the above again
- if the game has updated (compare size stored in file vs. size of actual exe, using "seek" -- faster than reading it all), run the above again and update the file
So with the logic above, the only 2 situations when the exe will be read/parsed/etc. are when the versioning file doesn't exist or when the game updates. The reason I am implementing it like this is the logic will be planted in "ui_main_menu.script", "main_menu:InitControls()" function. This, from what I tested, is executed twice when game starts (right before showing main menu). Then it's also executed every time you his Esc to show main menu while in-game.. and since the reading/parsing will create a 2-3s lag, you don't want that to happen every time cuz it will end-up annoying you.
From testing, I didn't need to store the version to a disk file or even check exe size because it's lightning fast the way I've implemented it:
Code: Select all
function GetGameVer()
local ver = "0.0.0"
file = io.open("xrEngine.exe", "rb")
if file then
local data = file:read("*all")
file:close()
local pattern = "[0-9]+%.+[0-9]+%.+[0-9]+%+"
for str in data:gmatch(pattern) do
if #str <= 7 then -- so it matches 1.8.0+ and future 1.10.0+ strings
ver = str:gsub("+", "")
break
end
end
end
return ver
end
So now, all I do is this:
Code: Select all
local game_name_str = "[ Shadow of Chornobyl "
local mod_version_str = " ][ In-Game: ESC S - Smart Save | ESC F1 - The Wish Granter ]"
..
function GetGameVer()
local ver = "0.0.0"
file = io.open("xrEngine.exe", "rb")
if file then
local data = file:read("*all")
file:close()
local pattern = "[0-9]+%.+[0-9]+%.+[0-9]+%+"
for str in data:gmatch(pattern) do
if #str <= 7 then -- so it matches 1.8.0+ and future 1.10.0+ strings
ver = str:gsub("+", "")
break
end
end
end
return ver
end
..
function main_menu:InitControls()
..
local _ver = xml:InitStatic("static_version", self)
_ver:SetText(game_name_str .. GetGameVer() .. mod_version_str)
..
end
And voila:
Will post more as I progress.