Module:Grind/GRON
From Fallen London Wiki
Documentation for this module may be created at Module:Grind/GRON/doc
local p = {}
local common = require('Module:Grind/common')
local util = require('Module:Grind/util')
local fml = require('Module:Grind/fml')
-- == GRON parsing & encoding utilities ==
-- Escape sequence expansion.
local function unescape(initial_s)
local s = ''
local esc = false
for pos = 1, #initial_s do
local c = initial_s:sub(pos, pos)
if esc then
if c == '"' or c == '\\' then
s = s .. c
else
s = s .. '\\' .. c
end
esc = false
else
if c == '\\' then
esc = true
else
s = s .. c
end
end
end
if esc then
s = s .. '\\'
end
return s
end
-- GRON format parser.
-- Returns Lua table representation of the object.
-- Returns `nil` if the string does not encode a valid GRON object.
function p.parse(s)
local gron = {}
local pos = 1 -- string position
local valc = 0 -- numbered values
pos = util.skip_whitespaces(s, pos)
-- expect (
if s:sub(pos, pos) ~= '(' then
return nil
end
local gron_end = util.find_match(s, pos)
if gron_end == nil then
return nil
end
pos = pos + 1
pos = util.skip_whitespaces(s, pos)
-- parse entries
while pos < gron_end do
pos = util.skip_whitespaces(s, pos)
local key = nil
local val = nil
local val_tuple = nil
-- parse the key
local current_char = s:sub(pos, pos)
if current_char == '"' then
local key_end = util.find_match(s, pos)
key = s:sub(pos + 1, key_end - 1)
key = unescape(key)
pos = key_end + 1
pos = util.skip_whitespaces(s, pos)
elseif current_char == '(' then
local key_end = util.find_match(s, pos)
key = p.parse(s:sub(pos, key_end))
pos = key_end + 1
pos = util.skip_whitespaces(s, pos)
else
key = ''
while current_char ~= ':' and current_char ~= ';' and current_char ~= ')' do
key = key .. current_char
pos, current_char = util.advance(s, pos)
end
key = util.strip_whitespaces(key)
end
local correct = false
current_char = s:sub(pos, pos)
if current_char == ')' then
-- the last entry is a numbered value
valc = valc + 1
gron[valc] = key
break
end
if current_char == ';' then
-- the entry is a numbered value
correct = true
valc = valc + 1
gron[valc] = key
pos = pos + 1
end
if current_char == ':' then
-- the entry is a key-values pair
correct = true
pos = pos + 1
pos = util.skip_whitespaces(s, pos)
val = ''
val_tuple = nil
current_char = s:sub(pos, pos)
while current_char ~= ';' and current_char ~= ')' do
local skip = false
if current_char == ',' then
if val_tuple == nil then
val_tuple = {}
end
table.insert(val_tuple, val)
val = ''
pos = pos + 1
pos = util.skip_whitespaces(s, pos)
current_char = s:sub(pos, pos)
skip = true
end
if not skip and current_char == '(' then
local val_gron_end = util.find_match(s, pos)
local val_gron = s:sub(pos, val_gron_end)
val = p.parse(val_gron)
pos = val_gron_end + 1
pos = util.skip_whitespaces(s, pos)
current_char = s:sub(pos, pos)
if current_char ~= ',' and current_char ~= ';' and current_char ~= ')' then
return nil
end
skip = true
end
if not skip and current_char == '"' then
local val_end = util.find_match(s, pos)
val = s:sub(pos + 1, val_end - 1)
val = unescape(val)
pos = val_end + 1
pos = util.skip_whitespaces(s, pos)
current_char = s:sub(pos, pos)
if current_char ~= ',' and current_char ~= ';' and current_char ~= ')' then
return nil
end
skip = true
end
if not skip then
while current_char ~= ',' and current_char ~= ';' and current_char ~= ')' do
val = val .. current_char
pos = pos + 1
current_char = s:sub(pos, pos)
end
val = util.strip_whitespaces(val)
end
end
-- the remaining val
if val ~= nil and val_tuple ~= nil then
table.insert(val_tuple, val)
val = nil
end
if val_tuple ~= nil then
gron[key] = val_tuple
else
gron[key] = val
end
if current_char == ')' then
break
end
pos = pos + 1
end
if not correct then
return nil
end
end
return gron
end
-- Encodes the string for string GRON representation.
-- Escape sequences and quotation marks are added where necessary.
function p.encode_string(str, only_contents)
only_contents = only_contents or false
if type(str) ~= 'string' then
return ''
end
if str == str:match('[%w%-_]*') then
return str
end
local s = ''
if not only_contents then
s = s .. '"'
end
for i = 1, #str do
local current_char = str:sub(i, i)
if current_char == '"' then
s = s .. '\\"'
elseif current_char == '\\' then
s = s .. '\\\\'
else
s = s .. current_char
end
end
if not only_contents then
s = s .. '"'
end
return s
end
-- Encodes GRON in string format.
-- The argument is the Lua table representation.
function p.encode(gron)
local s = '('
local first = true
for k, v in pairs(gron) do
if not first then
s = s .. ';'
end
first = false
if type(k) == 'number' then
if type(v) == 'table' then
s = s .. p.encode(v)
elseif type(v) == 'string' then
s = s .. p.encode_string(v)
elseif type(v) == 'number' then
s = s .. v
end
elseif type(k) == 'string' then
s = s .. p.encode_string(k) .. ':'
if type(v) == 'table' then
s = s .. p.encode(v)
elseif type(v) == 'string' then
s = s .. p.encode_string(v)
elseif type(v) == 'number' then
s = s .. v
end
end
end
return s .. ')'
end
-- Returns a table of declared GRON inputs.
function p.inputs(gron)
if type(gron) ~= 'table' then
return {}
end
local gtype = gron[1]
local inputs = {}
for _, v in pairs(gron) do
for k, t in pairs(p.inputs(v)) do
inputs[k] = t
end
end
if gtype == 'input' then
inputs[gron[2]] = gron['type'] or 'unknown'
end
if gtype == 'formula' then
local tree, err = fml.parse(gron[2])
if not err then
local vars = fml.variables(tree)
for v, _ in pairs(vars) do
if v:sub(1, 6) == 'Input:' then
local i, itype = v:sub(7), nil
i, itype = util.normalise_input(i)
if inputs[i] == 'unknown' and itype ~= 'unknown' then
-- now it is known
inputs[i] = itype
else
inputs[i] = inputs[i] or itype
end
end
end
end
end
return inputs
end
-- Returns a table of qualities used in GRON formulae.
local function formula_qualities(gron, qualities)
qualities = qualities or {}
if type(gron) ~= 'table' then
return qualities
end
local gtype = gron[1]
if gtype == 'formula' then
local tree, err = fml.parse(gron[2])
if not err then
local vars = fml.variables(tree)
for v, _ in pairs(vars) do
if v:sub(1, 6) ~= 'Input:' then
qualities[v] = qualities[v] or common.identify(v)
end
end
end
end
for k, v in pairs(gron) do
qualities = formula_qualities(v, qualities)
end
return qualities
end
-- Returns a table of possible GRON qualities used in challenges and requirements.
local function gron_qualities(gron, qualities)
qualities = qualities or {}
if type(gron) ~= 'table' then
return qualities
end
local gtype = gron[1]
if gtype == 'action' then
local challenge = gron['challenge']
if type(challenge) == 'table' then
for quality, data in pairs(challenge) do
qualities[quality] = common.identify(quality)
end
end
end
if gtype == 'req' then
local quality = gron['target']
if type(quality) == 'string' then
local already_unknown = qualities[quality] == 'unknown'
qualities[quality] = common.identify(quality)
local ranges = gron['ranges']
if not already_unknown and qualities[quality] == 'unknown' and type(ranges) == 'table' then
local only_binary = true
for _, range in ipairs(ranges) do
local rmin, rmax = util.parse_range(range)
if rmin ~= nil and rmax ~= nil then
only_binary = only_binary and (rmin == 0 or rmin == 1)
only_binary = only_binary and (rmax == 0 or rmax == 1 or rmax == 1/0)
end
end
if only_binary then
qualities[quality] = 'binary'
end
end
end
end
for _, v in pairs(gron) do
qualities = gron_qualities(v, qualities)
end
return qualities
end
-- Returns a table of qualities used in challenges, requirements, formulae.
function p.qualities(gron)
local qualities = gron_qualities(gron)
qualities = formula_qualities(gron, qualities)
return qualities
end
-- Returns a table of possible effect outputs (positive or negative).
local function effect_outputs(effect, blacklist)
if type(effect) ~= 'table' or effect[1] ~= nil then
return {}
end
local outputs = {}
for resource, _ in pairs(effect) do
if blacklist[resource] == nil then
outputs[resource] = true
end
end
return outputs
end
-- Returns a table of possible GRON outputs (positive or negative).
function p.outputs(gron, outputs, blacklist)
outputs = outputs or {}
blacklist = blacklist or {}
if type(gron) ~= 'table' then
return outputs
end
local gtype = gron[1]
if gtype == 'action' then
local alt_success_p = gron['alt_success_p'] or '0'
local alt_failure_p = gron['alt_failure_p'] or '0'
alt_success_p = tonumber(alt_success_p) or 0
alt_failure_p = tonumber(alt_failure_p) or 0
for resource, _ in pairs(effect_outputs(gron['success'], blacklist)) do
outputs[resource] = true
end
for resource, _ in pairs(effect_outputs(gron['failure'], blacklist)) do
outputs[resource] = true
end
if alt_success_p > 0 then
for resource, _ in pairs(effect_outputs(gron['alt_success'], blacklist)) do
outputs[resource] = true
end
end
if alt_failure_p > 0 then
for resource, _ in pairs(effect_outputs(gron['alt_failure'], blacklist)) do
outputs[resource] = true
end
end
end
if gtype == 'seq' or gtype == 'airs' or gtype == 'req' or gtype == 'best' then
for i = 2, #gron do
outputs = p.outputs(gron[i], outputs, blacklist)
end
end
if gtype == 'pswitch' then
local action_blacklist = mw.clone(blacklist)
local target = gron['target']
if type(target) == 'string' then
action_blacklist[target] = true
end
outputs = p.outputs(gron['action'], outputs, action_blacklist)
for i = 2, #gron do
outputs = p.outputs(gron[i], outputs, blacklist)
end
end
if gtype == 'gate' then
local action_blacklist = mw.clone(blacklist)
local target = gron['target']
if type(target) == 'table' and type(target[1]) == 'string' then
if not util.s2b(gron['preserve_progress'], false) then
action_blacklist[target[1]] = true
end
end
outputs = p.outputs(gron['action'], outputs, action_blacklist)
outputs = p.outputs(gron['resolution'], outputs, blacklist)
end
if gtype == 'filter' then
local target = gron['target'] or {}
if type(target) == 'string' then
target = {target}
end
local target_set = {}
if type(target) == 'table' then
for _, resource in ipairs(target) do
target_set[resource] = true
end
end
local action = gron[2]
local a_outputs = p.outputs(action, {}, blacklist)
for resource, _ in pairs(a_outputs) do
-- custom antiresources are to be added to (filter).target manually
if target_set[resource] or common.identify(resource) == 'menace' then
outputs[resource] = true
end
end
end
if gtype == 'sell' then
local action = gron['action']
local action_blacklist = mw.clone(blacklist)
for sell_resource, sell_effect in pairs(gron) do
if type(sell_resource) == 'string'
and sell_resource ~= 'action'
and sell_resource ~= 'optimise'
and sell_resource ~= 'market' then
-- Alas, we cannot filter out nonsense because the exact optimised action
-- will only be known during actual optimisation.
for resource, _ in pairs(effect_outputs(sell_effect, blacklist)) do
outputs[resource] = true
end
action_blacklist[sell_resource] = true
end
end
outputs = p.outputs(action, outputs, action_blacklist)
end
return outputs
end
return p