DXML
Intro
DXML by demonized#1084
DXML is a part of modded exes repo
DXML allows to manipulate XML files before they loaded into the engine or scripts by utilizing Lua The engine sends XML string to Lua where it is transformed into DOM-like object (from now on lets call it xml_obj) that can be manipulated by Lua methods and then it is converted back to XML string and sent back to engine
To use it in your mods, you have to create a new script file that is called modxml_<yourname>.script
. The yourname can be any string, it doesnt matter. The "modxml_" part must be in the filename.
Then, in this file, type:
function on_xml_read()
RegisterScriptCallback("on_xml_read", function(xml_file_name, xml_obj)
-- XML file i want to change
local xml_to_change = [[text\eng\_game_version.xml]]
-- Check if its the file i want to change
if xml_file_name == xml_to_change then
-- Here is my code to change XML
end
end)
end
What this does is creating a function on_xml_read that will be auto-called from dxml_core.script. This function will register function for new callback "on_xml_read", which accepts two arguments:
- xml_file_name - current XML filename that engine is processing (for example: text\eng_game_version.xml)
- xml_obj - the object described above
WARNING: DXML won't process translation strings other than from eng/rus folders and gameplay\character_desc_general.xml
file. For how to manipulate that file check "Additional functions" paragraph below.
To understand, what can be done with xml_obj and what functions it provides, let's take a look at typical usecases:
Case 1: inserting new XML data
Examples: insert new dialog into gameplay\dialogs.xml, insert new scope texture into ui\scopes.xml
This is the simplest case of using DXML, where you just want to insert new data, much like #include
directive in XML files
To do this, you can use xml_obj:insertFromXMLString
function as in example below:
function on_xml_read()
RegisterScriptCallback("on_xml_read", function(xml_file_name, xml_obj)
-- XML file i want to change
local xml_to_change = [[ui\scopes.xml]]
-- Check if its the file i want to change
if xml_file_name == xml_to_change then
-- Here is my code to change XML
local my_new_scope =
[[
<wpn_crosshair_bino x="0" y="0" width="2048" height="1536">
<auto_static x="0" y="0" width="1024" height="768" stretch="1">
<texture>wpn_crosshair_bino</texture>
#include "gameplay\character_criticals.xml"
</auto_static>
</wpn_crosshair_bino>
]]
xml_obj:insertFromXMLString(my_new_scope)
end
end)
end
This example adds new scope texture into scopes.xml.
DXML supports #include
directive to include other xml files into string. #include
must start from the beginning of the line like in the example.
(BTW: For actually adding new scope you don't need include anything, its just an example. The #include
is necessary when working with adding new special npcs or such)
insertFromXMLString method has these arguments:
- "xml_string" - XML string to process
- "where" - the element in xml_obj in which new data will be inserted argument is optional and specifies an element subtable of self.xml_table to insert (default - root element)
- "pos" argument is optional and specifies position to insert (default - to the end)
- "useRootNode" argument is optional and will hint DXML to insert contents inside the root node if it has one instead of whole string
The function returns the position of first inserted element in "where"
Case 1.1: inserting new XML data from file
Examples: insert new dialog into gameplay\dialogs.xml from file plugins\new_dialog.xml
This case will insert new data from a xml file plugins\new_dialog.xml
into gameplay\dialogs.xml
.
To do this, you can use xml_obj:insertFromXMLFile
function, where you specify the path to the file and the arguments, which are exactly the same as in xml_obj:insertFromXMLString
function.
The path argument should be a path to the file WITH EXTENSION (example: [[plugins\new_dialog.xml]]
).
The base folder for xml files to read is gamedata/configs, for example if the path provided is plugins\new_dialog.xml
, then the file should exist in gamedata/configs/plugins/new_dialog.xml
.
If the file has failed to read, the game will crash with the error message displaying what happened.
function on_xml_read()
RegisterScriptCallback("on_xml_read", function(xml_file_name, xml_obj)
-- XML file i want to change
local xml_to_change = [[gameplay\dialogs.xml]]
-- Check if its the file i want to change
if xml_file_name == xml_to_change then
-- Here is my code to change XML
xml_obj:insertFromXMLFile([[plugins\new_dialog.xml]])
end
end)
end
Case 2: inserting new XML data in specified element
Example: insert new menu item into ui\ui_mm_main.xml
In order to correctly insert new main menu item, we need to find the <menu_main> element first.
To find an element, you can utilize CSS-like selectors in query
function. An example of finding <menu_main> element would be:
local res = xml_obj:query("menu_main")
The function returns a table with all found elements matching this query.
The element is represented by table with following fields:
- el = element type, consists of type symbol, name and attributes. The type symbols are:
- "<" - node element
- "#" - text element
- parent = pointer to a table that contains this element
- kids = table that contains all children of this element
Then we check if table is not empty (the element exists) and insert our xml string with new menu item in position before the end
if is_not_empty(res) then
local el = res[1]
xml_obj:insertFromXMLString([[<btn name="btn_mcm" caption="ui_mm_menu_mcm"/>]], el, #el.kids)
Full code would be:
function on_xml_read()
RegisterScriptCallback("on_xml_read", function(xml_file_name, xml_obj)
-- XML file i want to change
local xml_to_change = [[ui\ui_mm_main.xml]]
-- Check if its the file i want to change
if xml_file_name == xml_to_change then
-- Here is my code to change XML
local mcm_menu = [[<btn name="btn_mcm" caption="ui_mm_menu_mcm" />]]
local res = xml_obj:query("menu_main")
if is_not_empty(res) then
local el = res[1]
xml_obj:insertFromXMLString(mcm_menu, el, #el.kids)
end
end
end)
end
But, if there is already an MCM menu, then you might get duplicated element inside. So we have to check if its already exists before insertion. We can check if element <btn name="btn_mcm">
exists before proceeding by utilizing query
again.
Here, similar to CSS, we want to find <btn>
element with attribute name="btn_mcm"
first. if its found - then make early return from the function
if is_not_empty(xml_obj:query("menu_main btn[name=btn_mcm]")) then
printf("MCM button already exists")
return
end
Full list of available CSS-like selectors are:
- " " (space) - find children of elemenents (can be any depth inside)
- ">" - find direct children
- "+" - find first sibling of element
- "~" - find all siblings
- "[attr1=value1]" - describes attribute
attr1
with valuevalue1
. To find element that matches multiple attributes you can use[attr1=value1][attr2=value2]
and so on
Case 3: Change text inside element
Example: change text of game version in text\eng\_game_version.xml
Getting and setting text is possible if element contains raw text inside, like <text attr1="value1">My Text</text>
.
To get text, you can use getText(element)
function and to set it use setText(element)
.
Example below of appending text to existing text in element
function on_xml_read()
RegisterScriptCallback("on_xml_read", function(xml_file_name, xml_obj)
if xml_file_name == [[text\eng\_game_version.xml]]
or xml_file_name == [[text\rus\_game_version.xml]]
then
-- Find string element with "id=ui_st_game_version" text inside it
local res = xml_obj:query("string[id=ui_st_game_version] > text")
if res[1] then
local el = res[1]
local el_text = xml_obj:getText(el)
if el_text then
-- Set new text
xml_obj:setText(el, el_text .. ". Modified exes (DLTX, DXML, Shader Scopes, SSS)")
end
end
end
end)
end
Case 4: changing attribute of element
Example: change position of money display in ui\ui_inventory.xml
First, find <money>
element inside <player>
element and then change its x, y attributes by using setElementAttr
function
setElementAttr(el, args)
- el - element found using
query
- args - table of attributes ({attr1 = value1, attr2 = value2})
To get attributes of element, use getElementAttr
function, it returns a table of attributes described above
getElementAttr(el)
- el - element found using
query
To remove attributes of element, use removeElementAttr
function
removeElementAttr(el, args)
- el - element found using
query
- args - list of attributes to remove ({attr1, attr2})
function on_xml_read()
RegisterScriptCallback("on_xml_read", function(xml_file_name, xml_obj)
if xml_file_name == [[ui\ui_inventory.xml]]
then
local res = xml_obj:query("player > money")
if res[1] then
local el = res[1]
xml_obj:setElementAttr(el, {x=20, y=60})
end
end
end)
end
Full list of methods is described in dxml_core.script in COnXmlRead
function
Additional functions
For now, it is impossible to use xml_obj from DXML to process gameplay\character_desc_general.xml
. This file includes all information about generic NPCs you see in the Zone (name, bio, community, etc). As an alternative, DXML provides additional callbacks in this case.
on_specific_character_init
on_specific_character_init
callback provides possibility to change NPCs' data. An example on how to use it:
function on_game_start()
RegisterScriptCallback("on_specific_character_init", function(character_id, data)
--character_id is the id attribute of <specific_character> tag (ie. "sim_default_csky_0_default_0")
if character_id == "sim_default_csky_0_default_0" then
-- change appearance of this npc to Beard
data.visual = "actors\stalker_neutral\stalker_neutral_3_face_1"
end
end)
end
Full list of fields available in "data" table:
- name
- bio
- community
- icon
- start_dialog
- panic_threshold
- hit_probability_factor
- crouch_type
- mechanic_mode
- critical_wound_weights
- supplies
- visual
- npc_config
- snd_config
- terrain_sect
- rank_min
- rank_max
- reputation_min
- reputation_max
- money_min
- money_max
- money_infinitive
on_specific_character_dialog_list
on_specific_character_dialog_list
callback provides possibility to change available dialogs for NPCs. The callback sends character id and dialog list class object, that can be used to manupulate available dialogs. An example on how to use it:
function on_game_start()
RegisterScriptCallback("on_specific_character_dialog_list", function(character_id, dialog_list)
--character_id is the id attribute of <specific_character> tag (ie. "sim_default_csky_0_default_0")
if character_id == "sim_default_csky_0_default_0" then
-- Add dialog about playing Blackjack
local res = dialog_list:add("cit_killers_minigame")
if res then
printf("adding dialog %s for %s, pos %s", "cit_killers_minigame", character_id, res)
end
end
end)
end
Full list of methods in dialog_list
- find(regex) - find existing dialog by regex, returns last found dialog matching regex and its position in the list
- has(string) - check if dialog by string exists, returns the position of found dialog in the list
- add(string, pos) - adds new dialog in specified position (by default adds before break dialog string if it exists)
- add_first(dialog) - adds dialog in the beginning of the list
- add_last(dialog) - adds dialog in the end of the list
- remove(string) - removes dialog by the string
- get_dialogs() - returns the list of all dialogs available
PS
See dxml_core.script
for all methods and callbacks available