Divergent/mods/Modded Executables/gamedata/scripts/dxml_core.script

1349 lines
35 KiB
Plaintext

-- 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 <actor_dialog> 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 <w> 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 <name, attr> 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/<path> 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 <actor_break_dialog> 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