S.T.A.L.K.E.R.: Shadow of Chornobyl - Enhanced Edition

Upload your cheat tables here (No requests)
Post Reply
User avatar
SunBeam
Administration
Administration
Posts: 4977
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 4716

S.T.A.L.K.E.R.: Shadow of Chornobyl - Enhanced Edition

Post by SunBeam »

Kindly DO NOT POST this table on other forums/communities (e.g.: Nexus, OCD, 3DM forums). Much like you prefer those places, I prefer FRF and I have created this exclusively for this community. If you want to spread the news and let others enjoy all of this, help them, etc. then please link them to this post on FRF instead. Thank you for respecting my choice!

[ 5 Jun 2025 - First Release ]

Game Name: S.T.A.L.K.E.R.: Shadow of Chornobyl - Enhanced Edition (short: SoCEE)
Game Version: 1.7.2
Game Process: xrEngine.exe / xrGame.dll
Game File Version: xrEngine.exe -- 1.7.2.13762 (1.7.2+28-3879775)



Hi folks,

The Enhanced Edition has been released for each of the 3 S.T.A.L.K.E.R. titles, which can be installed at not cost - if you own the original old games - from Steam. So here's me with some helpers, because I know you will be looking for them.


------------------------------------------------------------------------------------------------------------------------------------------------
[1] Extracting Game Data
------------------------------------------------------------------------------------------------------------------------------------------------


The Engine for all of three Enhanced Editions is based on the updated one used in Call of Prypiat (fork or the actual Open X-Ray), so all the knwon modding/editing that involves unpacking game content to the root folder, in "gamedata" sub-folder, is still applicable. To extract everything from the game's .db files you will require this:

db_unpacker.zip
Unpacker
(1.99 KiB) Downloaded 51 times

The unpacker was obtained from this link: [Link]. It is NOT MY WORK, also not that poster's work. I've yet to find the original post, but it's surely on that forum. Just so there's no haters barging in saying I am taking credit or whatever other hateful piss T_T. Digging a little bit more, I found some seemingly original link here: [Link]. It works for EE of each of the three titles as well, not just for True Stalker (a mod of CoP; posted about it here: viewtopic.php?f=4&t=35304).

Download the archive above and extract it to game root folder (e.g.: D:\SteamLibrary\steamapps\common\STALKER Shadow of Chornobyl - EE\), run game and at main menu:

- hit F1 key
- hit Tilde key so the console shows up
- hit Tilde key again to close the console
- hit A key to extract all data

Game will freeze, wait 2-3 minutes (I have a NVMe M.2 SSD), then check main folder (you can alt-tab any time, sure). Of course, you can then delete anything you don't need from the "unpack" folder. You will need 11.2 GB of disk space.

Once you're done with unpacking, you can remove the 'config' and 'scripts' folders in 'gamedata' folder, just so there's no confusion later with the next steps regarding the Cheat Menu. Please note that the unpacker was adjusted for SoCEE, so it will not work to download it from here and use it for Clear Sky - EE (short: CSEE) or Call of Prypiat - EE (short: CoPEE)! Download the unpackers from their respective topics on this forum, when made available!

I needed this unpacker to be able to slipstream the "Wish Granter" (or Cheat Menu). See 3:14 in the video below:




------------------------------------------------------------------------------------------------------------------------------------------------
[2] The Cheat Menu (in progress)
------------------------------------------------------------------------------------------------------------------------------------------------


------------------------------------------------------------------------------------------------------------------------------------------------
[3] The Cheat Table (in progress)
------------------------------------------------------------------------------------------------------------------------------------------------


Peace out,
Sun

How to use this cheat table?
  1. Install Cheat Engine
  2. Double-click the .CT file in order to open it.
  3. Click the PC icon in Cheat Engine in order to select the game process.
  4. Keep the list.
  5. Activate the trainer options by checking boxes or setting values from 0 to 1

User avatar
SunBeam
Administration
Administration
Posts: 4977
Joined: Sun Feb 04, 2018 7:16 pm
Reputation: 4716

Re: S.T.A.L.K.E.R.: Shadow of Chornobyl - Enhanced Edition

Post by SunBeam »

[ 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"):

Image

Image

Image

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:

Image

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:

Image

Uncomment the lines (removing the --[[ and ]]-- symbols) so it looks like this:

Image

Also change "DIK_S" to "DIK_F1". This way, F1 key will be our trigger key, instead of S:

Image

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:

Image

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:

Image

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:

Image

Save file and re-run game. Press F1 at main menu and this is what will happen:

Image

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:

Image

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>
Image

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:

Image

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:

Image

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:

Image

------------------------------------------------------------------------------------------------------------------------------------------------

[ 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:

Image

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:

Image

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:

Image

And this is the request for approval and permission granted:

Image

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:

Image

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:

Image

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:

Image

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:
  • open file
  • read file content
  • 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:

Image

Will post more as I progress.

Post Reply

Who is online

Users browsing this forum: BLEXBot, drymirage, MR17, NotAFedBoy, Odin46, xrenonx, Zeroneos