-- DXML Core script _G.MODDED_EXES_VERSION = get_modded_exes_version() -- IMPORTS local string_find = string.find local string_format = string.format local string_gmatch = string.gmatch local string_gsub = string.gsub local string_match = string.match local string_rep = string.rep local string_sub = string.sub local table_concat = table.concat local table_insert = table.insert local table_remove = table.remove local table_sort = table.sort local math_huge = math.huge -- UTILS -- str_explode with limit on items to split -- n is the amount of times to split -- str_explode("1,2,3,4", ",", 1) = {"1", "2,3,4"} _G.str_explode_lim = function(str, sep, n, plain) local n = n or math_huge if not (str and sep) then printe("!ERROR str_explode | missing parameter str = %s, sep = %s",str,sep) callstack() end if not (sep ~= "" and string_find(str,sep,1,plain) and n > 0) then return { str } end local t = {} local rest = {} local size = 0 for s in str:gsplit(sep,plain) do size = size + 1 if size > n then rest[#rest + 1] = s else t[size] = trim(s) end end if is_not_empty(rest) then local rest_s = table_concat(rest, sep) t[#t + 1] = rest_s end return t end -- Inserts character into string function string.insert(str1, str2, pos) return str1:sub(1,pos)..str2..str1:sub(pos+1) end -- Reduces multiple consecutive spaces in string to one _G.one_space = function(s) return string_gsub(s, " *", " ") end -- Removes newline characters from string _G.one_line = function(s) return s:gsub("[\n\r]", "") end -- Iterative print of tables with optional sorting for spairs, formatting is similar to PHP print_r _G.print_r = function(t, sort, sorting_func) if not t then return printf("nil") end local function vec_to_str(t) if t.w then return string_format("%s,%s,%s,%s", t.x, t.y, t.z, t.w) elseif t.z then return string_format("%s,%s,%s", t.x, t.y, t.z) elseif t.y then return string_format("%s,%s", t.x, t.y) elseif t.x then return string_format("%s", t.x) end end if type(t) ~= "table" then if type(t) == "userdata" then local vec = vec_to_str(t) if vec then return printf("%s", vec) end end return printf("%s", t) end if is_empty(t) then return printf("{}") end local function sort_func(t, a, b) if type(a) == "number" and type(b) == "number" then return a < b else return tostring(a) < tostring(b) end end sorting_func = sorting_func or sort_func local iterator_func = sort and spairs or pairs local print_r_cache={} local stack = {} local saved_pos = {} local indent = "" table_insert(stack, t) while #stack > 0 do local t = table_remove(stack) if not saved_pos[t] then saved_pos[t] = {} end local new_table = false for k, v in iterator_func(t, sort_func) do local key = k if type(k) == "userdata" then key = vec_to_str(k) or "userdata" end if saved_pos[t][k] then -- Do nothing elseif type(v) == "table" and print_r_cache[tostring(v)] then printf(indent.."["..key.."] => ".."*"..tostring(v)) elseif type(v) == "table" then table_insert(stack, t) table_insert(stack, v) printf(indent.."["..key.."] => "..tostring(v).." {") indent = indent..string_rep(" ", 2) new_table = true print_r_cache[tostring(t)] = true elseif type(v) == "userdata" then printf(indent.."["..key.."] => "..(vec_to_str(v) or "userdata")) else printf(indent.."["..key.."] => "..tostring(v)) end saved_pos[t][k] = true if new_table then break end end print_r_cache[tostring(t)] = true if not new_table then indent = indent:sub(1, -3) if #stack > 0 then printf(indent.."}") end -- saved_pos[t] = nil end end printf(" ") end _G.try = function(func, ...) local status, error_or_result = pcall(func, ...) if not status then printf(error_or_result) return false, status, error_or_result else return error_or_result, status end end -- Postpone on next n tick _G.nextTick = function(f, n) n = math.floor(math.max(n or 1, 1)) AddUniqueCall(function() if n == 1 then return f() else n = n - 1 return false end end) end -- Throttle function to fire every tg_throttle milliseconds _G.throttle = function(func, tg_throttle) local tg = 0 if not tg_throttle or tg_throttle == 0 then return function(...) local t = time_global() if t ~= tg then tg = t return func(...) end end else return function(...) local t = time_global() if t < tg then return end tg = t + tg_throttle return func(...) end end end _G.print_tip = function(text, ...) local text = tostring(text) printf(text, ...) if not db.actor then return end local ico = "ui_inGame2_Dengi_otdani" local text_color = utils_xml.get_color("pda_white") local arg_color = utils_xml.get_color("d_green") local function colorize(s) return arg_color .. s .. text_color end local i = 0 local t = {...} if #t > 0 then local function sr(a) i = i + 1 if (type(t[i]) == 'userdata') then if (t[i].x and t[i].y) then return colorize(vec_to_str(t[i])) end return colorize('userdata') end return colorize(tostring(t[i])) end text = string.gsub(game.translate_string(text), "%%s", sr) else text = game.translate_string(text) end text = text_color .. text news_manager.send_tip(db.actor, text, nil, ico, 6000) end -- Patches local xmlCallbacks = {} RSC = _G.RegisterScriptCallback _G.RegisterScriptCallback = function(name,func_or_userdata) if name == "on_xml_read" then for i = #xmlCallbacks, 1, -1 do local v = xmlCallbacks[i] if v == func_or_userdata then return end end table.insert(xmlCallbacks, func_or_userdata) return end RSC(name, func_or_userdata) end URSC = _G.UnregisterScriptCallback _G.UnregisterScriptCallback = function(name,func_or_userdata) if name == "on_xml_read" then for i = #xmlCallbacks, 1, -1 do local v = xmlCallbacks[i] if v == func_or_userdata then table.remove(xmlCallbacks, i) end end return end URSC(name, func_or_userdata) end function xmlDispatch(xml_file_name, xml_obj) for i, v in ipairs(xmlCallbacks) do v(xml_file_name, xml_obj) end end -- DXML MAIN assert(AddScriptCallback, "Cannot add callbacks to the game, reinstall Anomaly 1.5.2 and Modded Exes") AddScriptCallback("on_specific_character_dialog_list") AddScriptCallback("on_specific_character_init") AddScriptCallback("on_xml_read") local function dialogs_obj(dialog_list) local d = dialog_list local cl = {} cl.find = function(self, dialog) for i = #d, 1, -1 do if string_find(d[i], dialog) then return d[i], i end end end cl.has = function(self, dialog) for i = #d, 1, -1 do if d[i] == dialog then return i end end end cl.add = function(self, dialog, pos) if self:has(dialog) then return end if not pos then local break_dialog, break_pos = self:find("break_dialog") if break_pos then pos = break_pos else pos = #d + 1 end else pos = clamp(pos, 1, #d + 1) end table_insert(d, pos, dialog) return pos end cl.add_first = function(self, dialog) return self:add(dialog, 1) end cl.add_last = function(self, dialog) return self:add(dialog, #d + 1) end cl.remove = function(self, dialog) local pos = self:has(dialog) if pos then local el = table_remove(d, pos) return el, pos end end cl.get_dialogs = function(self) return dup_table(d) end return cl end -- Called from specific_character.cpp -- Allows to manipulate available actor dialog list defined in characted_desc...xml files in tags _G.CSpecificCharacterDialogList = function(character_id, dialog_list) SendScriptCallback("on_specific_character_dialog_list", character_id, dialogs_obj(dialog_list)) return dialog_list end -- Called from specific_character.cpp -- Allows to manipulate specific character data from characted_desc...xml files -- Available fields in "data" table, similar to defined in xmls: --[[ 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 --]] _G.CSpecificCharacterInit = function(character_id, data) SendScriptCallback("on_specific_character_init", character_id, data) return data end -- Manipulate xml nodes through modxml_...script files -- Gather all files do printf("init gathering modxml_... scripts") local ignore = { ["_g.script"] = true, } local t = {} local size_t = 0 local f = getFS() local flist = f:file_list_open_ex("$game_scripts$",bit_or(FS.FS_ListFiles,FS.FS_RootOnly),"modxml_*.script") local f_cnt = flist:Size() for it=0, f_cnt-1 do local file = flist:GetAt(it) local file_name = file:NameShort() --printf("%s size=%s",file_name,file:Size()) if (file:Size() > 0 and ignore[file_name] ~= true) then file_name = file_name:sub(0,file_name:len()-7) if (_G[file_name] and _G[file_name].on_xml_read) then printf("gathering %s.script", file_name) size_t = size_t + 1 t[size_t] = file_name -- load all scripts first end end end table_sort(t) for i=1, #t do local file_name = t[i] _G[file_name].on_xml_read() end -- Force load some other non modxml scripts local force_load = { ["ui_options_modded_exes"] = true } k2t_table(force_load) table_sort(force_load) for _, k in ipairs(force_load) do if (_G[k] and _G[k].on_xml_read) then local s = _G[k] s.on_xml_read() end end end -- Cache parsed files local xml_string_cache_enabled = false local xml_string_debug_enabled = false local xml_string_cache = {} local function print_xml_error(xml_file_name, s, ...) return printf("!DXML Error, file " .. xml_file_name .. " : " .. s, ...) end local function print_parser_msg(xml_file_name, s, ...) return printf("DXML Parser, file " .. xml_file_name .. " : " .. s, ...) end local function print_parser_error(xml_file_name, s, ...) return printf("!DXML Parser Error, file " .. xml_file_name .. " : " .. s, ...) end local function print_r_xml(...) if xml_string_debug_enabled then return print_r(...) end end local function xml_walk(walk_table, callback) for i, v in ipairs(walk_table) do if callback(v, i, walk_table) then return end if v.kids then xml_walk(v.kids, callback) end end end _G.table_walk = function(walk_table, callback) for k, v in pairs(walk_table) do if callback(v, k, walk_table) then return end if type(v) == "table" then table_walk(v, callback) end end end _G.table_merge = function(t1, ...) local ts = {...} for i, t in ipairs(ts) do for k, v in pairs(t) do t1[k] = v end end return t1 end _G.table_equals = function(actual, expected, cycleDetectTable) local type_a, type_e = type(actual), type(expected) if type_a ~= type_e then return false -- different types won't match end if type_a ~= 'table' then -- other types compare directly return actual == expected end cycleDetectTable = cycleDetectTable or { actual={}, expected={} } if cycleDetectTable.actual[ actual ] then -- oh, we hit a cycle in actual if cycleDetectTable.expected[ expected ] then -- uh, we hit a cycle at the same time in expected -- so the two tables have similar structure return true end -- cycle was hit only in actual, the structure differs from expected return false end if cycleDetectTable.expected[ expected ] then -- no cycle in actual, but cycle in expected -- the structure differ return false end -- at this point, no table cycle detected, we are -- seeing this table for the first time -- mark the cycle detection cycleDetectTable.actual[ actual ] = true cycleDetectTable.expected[ expected ] = true local actualKeysMatched = {} for k, v in pairs(actual) do actualKeysMatched[k] = true -- Keep track of matched keys if not table_equals(v, expected[k], cycleDetectTable) then -- table differs on this key -- clear the cycle detection before returning cycleDetectTable.actual[ actual ] = nil cycleDetectTable.expected[ expected ] = nil return false end end for k, v in pairs(expected) do if not actualKeysMatched[k] then -- Found a key that we did not see in "actual" -> mismatch -- clear the cycle detection before returning cycleDetectTable.actual[ actual ] = nil cycleDetectTable.expected[ expected ] = nil return false end -- Otherwise actual[k] was already matched against v = expected[k]. end -- all key match, we have a match ! cycleDetectTable.actual[ actual ] = nil cycleDetectTable.expected[ expected ] = nil return true end local parser = slaxml.SLAXML() local parser_options = {stripWhitespace = true} function xml_object(xml_file_name, xml_string, xml_table) local t = { xml_string = xml_string, xml_table = xml_table, } -- Check if node is Processing Instruction t.isPi = function(self, el) return el.el:sub(1, 1) == "?" end -- Check if node is DOM element t.isElement = function(self, el) return el.el:sub(1, 1) == "<" end -- Check if node is text t.isText = function(self, el) return el.el:sub(1, 1) == "#" end -- Get element name t.getElementName = function(self, el) if self:isElement(el) then local name = str_explode(el.el:sub(2), " ") return name[1] end end -- Get element name and attributes -- If attribute has value - it will be string type, convert to number in your scripts if you need to t.getElement = function(self, el) if self:isElement(el) then local res = {} local e = str_explode_lim(el.el:sub(2), " ", 1) res.name = e[1] res.attr = {} if e[2] then local attrs = str_explode(e[2], " ") for i, attr in ipairs(attrs) do local a = str_explode(attr, "=") if not a[2] then res.attr[a[1]] = true else res.attr[a[1]] = a[2] end end end return res end end -- Get xml root -- If xml has single child element of !doc node (for example tag in ui_options.xml), this child will be considered the root -- Otherwise the root will be !doc node t.getRoot = function(self) local root_pos = 1 if is_empty(self.xml_table.kids) then return self.xml_table end if is_empty(self.xml_table.kids[root_pos]) then return self.xml_table end if self:isPi(self.xml_table.kids[root_pos]) then root_pos = 2 end if self.xml_table.kids[root_pos].el then if self.xml_table.kids[root_pos + 1] then return self.xml_table end return self.xml_table.kids[root_pos] end return self.xml_table end -- Find element by information in args table -- args table may have structures: --[[ args = { name = "element_name", attr = { attr1 = 32, attr2 = "myattr", } } OR (for multiple query search) args = { { name = "element_name", attr = { attr1 = 32, attr2 = "myattr", }, }, { name = "element_name2", attr = { attr1 = 544, }, }, ... } --]] -- one of fields must exist in args table -- where argument is optional and specifies sub-table in self.xml_table to search -- returns table that contains elements sub-tables from self.xml_table that matches args t.findElement = function(self, args, where) if is_empty(args) then print_parser_error(xml_file_name, "args query table is empty") return end -- Convert to multiple args table if args.name or args.attr then args = {args} end where = where and where.kids or self:getRoot().kids local res = {} xml_walk(where, function(v) local el = self:getElement(v) if not el then return end for i = 1, #args do local arg = args[i] if self:checkElement(v, arg) then res[#res + 1] = v break end end end) if is_empty(res) then if xml_string_debug_enabled then print_parser_error(xml_file_name, "no elements were found with supplied query") print_r_xml(args) end return end return res end -- Check if element is fulfilling the args table query -- Args structure is similar to findElement function t.checkElement = function(self, el, args) -- Filter element to match query local check_table = {} local arg = args local v = el local el = self:getElement(v) if not el then return end for key, value in pairs(el) do if arg[key] then check_table[key] = value end end -- If arg table and filtered element is equal - found element local found = true if arg.name and arg.name ~= check_table.name then found = false elseif check_table.attr then for arg_key, arg_value in pairs(arg.attr) do if arg_value ~= check_table.attr[arg_key] then found = false end end end if found then return el end end -- Iterate children elements of element -- callback is function(v, i), v is current child element, i - child number -- return true in callback function will stop iteration t.iterateChildren = function(self, el, callback) if not callback then print_parser_error(xml_file_name, "callback not specified for iterateChildren") return end for i, v in ipairs(el.kids) do if callback(v, i) then return end end end -- Find siblinds of the element by information in args table -- Args structure is similar to findElement function t.findSiblings = function(self, el, args, first) if not el.parent then return end -- Convert to multiple args table if args.name or args.attr then args = {args} end local res = {} local el_pos self:iterateChildren(el.parent, function(v, i) if v == el then el_pos = i return end if el_pos and i > el_pos then for i = 1, #args do local arg = args[i] if self:checkElement(v, arg) then res[#res + 1] = v return first end end end end) if is_empty(res) then print_parser_error(xml_file_name, "no siblings were found with supplied query") print_r_xml(args) return end return res end -- Find closest sibling of the element by information in args table -- Args structure is similar to findElement function t.findFirstSibling = function(self, el, args) return self:findSiblings(el, args, true) end local function concat_attr(attr, sep) if is_empty(attr) then return "" end sep = sep or " " local res = "" for k, v in pairs(attr) do res = res .. sep .. k .. '=' .. v end return res:sub(sep:len() + 1) end -- Get position of element relative to others t.getElementPosition = function(self, el) local pos self:iterateChildren(el.parent, function(v, i) if v == el then pos = i return true end end) return pos end -- Convert table structure of element to suitable for xml_table -- Args structure is similar to t.convertElement = function(self, args) if is_empty(args) or not args.name then print_parser_error(xml_file_name, "you trying to convert empty table") return end args.attr = args.attr or {} local attr_string = concat_attr(args.attr) local res = { el = "<" .. args.name .. (attr_string:len() > 0 and (" " .. attr_string) or ""), kids = args.kids or {}, } return res end -- Gets attributes of element t.getElementAttr = function(self, el) return self:getElement(el).attr end t.setElementAttr = function(self, el, args) if is_empty(args) then print_parser_error(xml_file_name, "you trying to set empty arguments to element") return end local data = self:getElement(el) if not data.attr then data.attr = {} end for k, v in pairs(args) do data.attr[k] = v end local convertedData = self:convertElement(data) el.el = convertedData.el end -- Removes attributes of element -- Args is a list of attributes to remove {"attr1", "attr2"} t.removeElementAttr = function(self, el, args) if is_empty(args) then print_parser_error(xml_file_name, "you trying to remove arguments to element") return end for i, v in ipairs(args) do args[v] = true args[i] = nil end local data = self:getElement(el) if not data.attr then return end for k, v in pairs(args) do if v then data.attr[k] = nil end end local convertedData = self:convertElement(data) el.el = convertedData.el end -- Insert element into xml table -- where 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 - in the end of node) t.insertElement = function(self, args, where, pos) if is_empty(args) then print_parser_error(xml_file_name, "you are trying to insert empty table") return end if not (args.name or args.el) then print_parser_error(xml_file_name, "you are trying to insert non-element node") print_r_xml(args) return end where = where or self:getRoot() if not where.kids then where.kids = {} end if args.name then args = self:convertElement(args) end pos = pos and clamp(pos, 1, #where.kids + 1) or #where.kids + 1 table_insert(where.kids, pos, args) return where.kids[pos], pos end -- Insert new element before element into xml table t.insertElementBefore = function(self, args, el, after) if is_empty(args) then print_parser_error(xml_file_name, "you are trying to insert empty table") return end if not (args.name or args.el) then print_parser_error(xml_file_name, "you are trying to insert non-element node") print_r_xml(args) return end if args.name then args = self:convertElement(args) end local pos self:iterateChildren(el.parent, function(v, i) if v == el then pos = after and i + 1 or i table_insert(el.parent, pos, args) return true end end) if not pos then print_parser_error(xml_file_name, "failed to insert element " .. (after and "after" or "before") .. "specified element") print_parser_error(xml_file_name, "input element") print_r_xml(args) print_parser_error(xml_file_name, "target element") print_r_xml(el) return end return el[pos], pos end -- Insert new element after element into xml table t.insertElementAfter = function(self, args, el) return self:insertElementBefore(args, el, true) end -- Parse xml from string into table similar to xml_table t.parseXMLString = function(self, xml_string) local dom = parser:simple_dom(xml_string, {stripWhitespace = true, parseIncludes = true}) return xml_object("parseXMLString", xml_string, dom) end -- Insert from XML string into xml_table -- where 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 -- Returns the position of first inserted element in "where" -- This function is identical to #include directive in xml files t.insertFromXMLString = function(self, xml_string, where, pos, useRootNode) if xml_string == "" then return end where = where or self:getRoot() local xml_obj = self:parseXMLString(xml_string) local xml_obj_root = useRootNode and xml_obj:getRoot() or xml_obj.xml_table local n_pos for i, v in ipairs(xml_obj_root.kids) do v.parent = where local ins = pos and clamp(pos, 1, #where.kids + 1) or #where.kids + 1 local p = table_insert(where.kids, ins, v) if not n_pos then n_pos = p end end return where.kids[n_pos], n_pos end -- Insert from XML file -- path argument should be a path to the file WITH EXTENSION (example: "gameplay\\npc_profiles.xml") -- This function opens XML file inside gamedata/configs/ folder and inserts the contents through insertFromXMLString function t.insertFromXMLFile = function(self, path, where, pos, useRootNode) local p = getFS():update_path('$game_config$', '') .. path local f, status, code = io.open(p, "r") assert(f, ("\n\nDXML ERROR: insertFromXMLFile\nCan't read file by path %s %s"):format(p, status and status:gsub(p, "") or "nil")) local contents = f:read('*all') f:close() return self:insertFromXMLString(contents, where, pos, useRootNode) end -- Get text inside of element -- Returns text if element contains only #text node and prints errors otherwise t.getText = function(self, el) if is_empty(el.kids) then print_parser_error(xml_file_name, "element %s doesnt have children", el.el) print_r_xml(el) return end if #el.kids > 1 then print_parser_error(xml_file_name, "element %s is not text only", el.el) print_r_xml(el) return end if not self:isText(el.kids[1]) then print_parser_error(xml_file_name, "element %s child is not text node", el.el) print_r_xml(el) return end return el.kids[1].el:sub(2) end -- Set text inside of element t.setText = function(self, el, text) if not self:isElement(el) then print_parser_error(xml_file_name, "you are trying to insert text into non-element node") print_r_xml(el) return end if not el.kids then el.kids = {} end if is_empty(el.kids) then el.kids[1] = { el = "#" } end if self:getText(el) then el.kids[1].el = "#" .. text end end -- Removes element from xml_table -- the el argument is a sub-table from xml_table which is received by findElement function -- in other words, the pointers should be same, otherwise it wont do anything t.removeElement = function(self, el) local res, pos self:iterateChildren(el.parent, function(v, i) if v == el then res = table_remove(el.parent.kids, i) pos = i return true end end) return res, pos end -- Find element by CSS-like selector local function cssQuery(query) -- Parse query local query_table = {} local function add_element(type, value) table_insert(query_table, { type = type, value = value, attr = {}, }) end local current_element = "" local current_selector = "" local current_attr_key = "" local current_attr_value = "" local q_state = "" query = trim(one_space(query)) for i = 1, query:len() + 1 do local c = query:sub(i, i) if q_state == "" then if c:find("[%a_]") then q_state = "element" current_element = current_element .. c end elseif q_state == "element" then if c:find("[%w_]") then -- Still reading element current_element = current_element .. c elseif c:find("[%s>+~]") then q_state = "selector" add_element("element", current_element) current_selector = c current_element = "" elseif c == "[" then q_state = "attr" add_element("element", current_element) current_element = "" elseif c == "" then add_element("element", current_element) end elseif q_state == "attr" then if c == "]" then q_state = "attr_end" elseif c:find("[%a_]") then q_state = "attr_key" current_attr_key = current_attr_key .. c end elseif q_state == "attr_key" then if c == "=" then q_state = "attr_value" elseif c:find("[%w_]") then -- Still reading attribute key current_attr_key = current_attr_key .. c end elseif q_state == "attr_value" then if c == "]" then q_state = "attr_end" current_attr_value = trim(current_attr_value) if current_attr_value:sub(1,1):find("['\"]") then current_attr_value = current_attr_value:sub(2) end if current_attr_value:sub(-1):find("['\"]") then current_attr_value = current_attr_value:sub(1, -2) end query_table[#query_table].attr[current_attr_key] = current_attr_value current_attr_key = "" current_attr_value = "" else current_attr_value = current_attr_value .. c end elseif q_state == "attr_end" then if c:find("[%s>+~]") then q_state = "selector" current_selector = c elseif c == "[" then q_state = "attr" end elseif q_state == "selector" then if c:find("[>+~]") then current_selector = c elseif c == "[" then q_state = "attr" elseif c:find("[%a_]") then add_element("selector", current_selector) q_state = "element" current_element = current_element .. c end end end return query_table end t.selector_functions = { [" "] = function(xml_obj, args, where) local res = xml_obj:findElement(args, where) return is_not_empty(res) and res or {} end, [">"] = function(xml_obj, args, where) local res = {} xml_obj:iterateChildren(where, function(v, i) if xml_obj:checkElement(v, args) then table_insert(res, v) end end) return res end, ["+"] = function(xml_obj, args, where) local res = {} local r = xml_obj:findFirstSibling(where, args) if r then res[1] = r end return res end, ["~"] = function(xml_obj, args, where) local res = xml_obj:findSiblings(where, args) return is_not_empty(res) and res or {} end, } t.query = function(self, query, where) local query_table = cssQuery(query) if is_empty(query_table) then callstack() print_parser_error(xml_file_name, "invalid query %s", query) assert(false, ("\n\n!DXML ERROR invalid query %s"):format(query)) return {} end local stack = {where or self:getRoot()} local selector = self.selector_functions[" "] for i, v in ipairs(query_table) do if v.type == "element" then local new_stack = {} for _, el in ipairs(stack) do local res = selector(self, { name = v.value, attr = v.attr }, el) if is_empty(res) then break end for i, v in ipairs(res) do table_insert(new_stack, v) end end stack = new_stack if is_empty(stack) then if xml_string_debug_enabled then print_parser_error(xml_file_name, "no elements were found matching query %s", query) end return {} end elseif v.type == "selector" then selector = self.selector_functions[v.value] if not selector then print_parser_error(xml_file_name, "encountered unknown selector %s in query %s, abort", v.value, query) return {} end end end return stack end -- Some convenient functions targeting specific XMLs -- Add unique actor_dialog line for specific_character element in character_desc -- pos is optional and defines where to insert the line (default - before line) t.insertActorDialog = function(self, character_id, dialog_line, pos) if not string_find(xml_file_name, "character_desc_") then return end local el = self:query(string_format("specific_character[id=%s]", character_id)) el = el[1] if xml_string_debug_enabled then print_parser_msg(xml_file_name, "found specific_character by id %s", character_id) end local already_has_dialog local actor_break_dialog_pos self:iterateChildren(el, function(v, i) if self:getElementName(v) == "actor_dialog" then local d = self:getText(v) if d == dialog_line then print_parser_error(xml_file_name, "character_id %s already has actor_dialog %s", character_id, dialog_line) already_has_dialog = true return true end if d:find("break_dialog") then if xml_string_debug_enabled then print_parser_msg(xml_file_name, "character_id %s found actor_break_dialog at %s", character_id, i) end actor_break_dialog_pos = i end end end) if not already_has_dialog then pos = pos and clamp(pos, 1, #el.kids + 1) or actor_break_dialog_pos if xml_string_debug_enabled then print_parser_msg(xml_file_name, "character_id %s inserting actor_dialog %s, pos %s", character_id, dialog_line, pos) end local n_el, n_pos = self:insertElement({name = "actor_dialog"}, el, pos) if n_el then if xml_string_debug_enabled then print_parser_msg(xml_file_name, "character_id %s success inserted actor_dialog %s, pos %s", character_id, dialog_line, n_pos) end self:setText(n_el, dialog_line) return n_el, n_pos else print_parser_error(xml_file_name, "character_id %s failed to insert actor_dialog %s, pos %s", character_id, dialog_line, pos) return end end end return t end -- Called from ScriptXMLInit.cpp -- Allows to manipulate xml content through xml parser _G.COnXmlRead = function(xml_file_name, xml_string) -- Do not process language strings other than "eng/rus", no support yet if xml_file_name:find([[^text\]]) and not ( xml_file_name:match([[^text\(eng)\]]) or xml_file_name:match([[^text\(rus)\]]) ) then return xml_string end -- Special case for character_desc_general.xml files since it can be huge if xml_file_name == [[gameplay\character_desc_general.xml]] then return xml_string end if xml_string_cache_enabled and xml_string_cache[xml_file_name] then if xml_string_debug_enabled then print_parser_msg(xml_file_name, "cache found, load cache") end return xml_string_cache[xml_file_name] end if xml_string_debug_enabled then print_parser_msg(xml_file_name, "loaded") end local xml_table = try(parser.simple_dom, parser, xml_string, parser_options) if not xml_table then print_parser_error(xml_file_name, "error parsing") return xml_string end local xml_obj = try(xml_object, xml_file_name, xml_string, xml_table) if not xml_obj then print_parser_error(xml_file_name, "error creating xml_obj") return xml_string end if xml_string_debug_enabled then print_parser_msg(xml_file_name, "parsed") end xmlDispatch(xml_file_name, xml_obj) if xml_string_debug_enabled then print_parser_msg(xml_file_name, "callback") end xml_string = parser:simple_xml(xml_table) -- Cache parsed file if xml_string_cache_enabled then xml_string_cache[xml_file_name] = xml_string end return xml_string end -- UTILS -- Open file, apply dxml edits and return the xml_obj for custom work with files outside of callback function openXMLFile(xml_file_name) local function loadFile(path) local file_reader = getFS():r_open('$game_config$', path) if file_reader then local lua_s = "" while not s:r_eof() do lua_s = lua_s .. string.char(s:r_u8()) end return lua_s end end local xml_string = loadFile(xml_file_name) if not xml_string then printf("error opening file %s", xml_file_name) return end local parser = slaxml.SLAXML() local parser_options = {stripWhitespace = true} local xml_table = try(parser.simple_dom, parser, xml_string, parser_options) if not xml_table then printf("error parsing file %s", xml_file_name) return end local xml_obj = try(xml_object, xml_file_name, xml_string, xml_table) if not xml_obj then printf("error creating xml_obj for file %s", xml_file_name) return end xmlDispatch(xml_file_name, xml_obj) return xml_obj end