mirror of
https://github.com/Reuh/anselme.git
synced 2025-12-14 04:09:08 +00:00
Handle events in text interpolation; capture text events in choice lines; improve test script
This commit is contained in:
parent
633f7b2d61
commit
7105b445ef
103 changed files with 2452 additions and 1294 deletions
|
|
@ -3,7 +3,7 @@ local eval
|
|||
|
||||
local common
|
||||
common = {
|
||||
-- flush interpreter state to global state
|
||||
--- merge interpreter state with global state
|
||||
merge_state = function(state)
|
||||
local global_vars = state.interpreter.global_state.variables
|
||||
for var, value in pairs(state.variables) do
|
||||
|
|
@ -11,7 +11,7 @@ common = {
|
|||
state.variables[var] = nil
|
||||
end
|
||||
end,
|
||||
-- returns a variable's value, evaluating a pending expression if neccessary
|
||||
--- returns a variable's value, evaluating a pending expression if neccessary
|
||||
-- if you're sure the variable has already been evaluated, use state.variables[fqm] directly
|
||||
-- return var
|
||||
-- return nil, err
|
||||
|
|
@ -28,7 +28,7 @@ common = {
|
|||
return var
|
||||
end
|
||||
end,
|
||||
-- check truthyness of an anselme value
|
||||
--- check truthyness of an anselme value
|
||||
truthy = function(val)
|
||||
if val.type == "number" then
|
||||
return val.value ~= 0
|
||||
|
|
@ -38,7 +38,7 @@ common = {
|
|||
return true
|
||||
end
|
||||
end,
|
||||
-- compare two anselme value for equality
|
||||
--- compare two anselme value for equality
|
||||
compare = function(a, b)
|
||||
if a.type ~= b.type then
|
||||
return false
|
||||
|
|
@ -59,10 +59,10 @@ common = {
|
|||
return a.value == b.value
|
||||
end
|
||||
end,
|
||||
-- format a anselme value to something printable
|
||||
--- format a anselme value to something printable
|
||||
-- does not call custom {}() functions, only built-in ones, so it should not be able to fail
|
||||
-- str: if success
|
||||
-- * nil, err: if error
|
||||
-- nil, err: if error
|
||||
format = function(val)
|
||||
if atypes[val.type] and atypes[val.type].format then
|
||||
return atypes[val.type].format(val.value)
|
||||
|
|
@ -70,8 +70,9 @@ common = {
|
|||
return nil, ("no formatter for type %q"):format(val.type)
|
||||
end
|
||||
end,
|
||||
--- convert anselme value to lua
|
||||
-- lua value: if success (may be nil!)
|
||||
-- * nil, err: if error
|
||||
-- nil, err: if error
|
||||
to_lua = function(val)
|
||||
if atypes[val.type] and atypes[val.type].to_lua then
|
||||
return atypes[val.type].to_lua(val.value)
|
||||
|
|
@ -79,8 +80,9 @@ common = {
|
|||
return nil, ("no Lua exporter for type %q"):format(val.type)
|
||||
end
|
||||
end,
|
||||
--- convert lua value to anselme
|
||||
-- anselme value: if success
|
||||
-- * nil, err: if error
|
||||
-- nil, err: if error
|
||||
from_lua = function(val)
|
||||
if ltypes[type(val)] and ltypes[type(val)].to_anselme then
|
||||
return ltypes[type(val)].to_anselme(val)
|
||||
|
|
@ -88,23 +90,36 @@ common = {
|
|||
return nil, ("no Lua importer for type %q"):format(type(val))
|
||||
end
|
||||
end,
|
||||
--- evaluate a text AST into a single Lua string
|
||||
-- string: if success
|
||||
-- * nil, err: if error
|
||||
-- nil, err: if error
|
||||
eval_text = function(state, text)
|
||||
local s = ""
|
||||
local l = {}
|
||||
common.eval_text_callback(state, text, function(str) table.insert(l, str) end)
|
||||
return table.concat(l)
|
||||
end,
|
||||
--- same as eval_text, but instead of building a Lua string, call callback for every evaluated part of the text
|
||||
-- callback returns nil, err in case of error
|
||||
-- true: if success
|
||||
-- nil, err: if error
|
||||
eval_text_callback = function(state, text, callback)
|
||||
for _, item in ipairs(text) do
|
||||
if type(item) == "string" then
|
||||
s = s .. item
|
||||
callback(item)
|
||||
else
|
||||
local v, e = eval(state, item)
|
||||
if not v then return v, e end
|
||||
v, e = common.format(v)
|
||||
if not v then return v, e end
|
||||
s = s .. v
|
||||
if v ~= "" then
|
||||
local r, err = callback(v)
|
||||
if err then return r, err end
|
||||
end
|
||||
end
|
||||
end
|
||||
return s
|
||||
return true
|
||||
end,
|
||||
--- check if an anselme value is of a certain type
|
||||
-- specificity(number): if var is of type type
|
||||
-- false: if not
|
||||
is_of_type = function(var, type)
|
||||
|
|
|
|||
|
|
@ -350,7 +350,7 @@ local function eval(state, exp)
|
|||
ret, e = run(state, fn.child)
|
||||
-- resume at last checkpoint
|
||||
else
|
||||
local expr, err = expression(checkpoint.value, state, "")
|
||||
local expr, err = expression(checkpoint.value, state, fn.namespace)
|
||||
if not expr then return expr, err end
|
||||
ret, e = eval(state, expr)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
local eval
|
||||
local truthy, merge_state, to_lua, eval_text, escape, get_variable
|
||||
local truthy, merge_state, to_lua, escape, get_variable, eval_text_callback
|
||||
local run_line, run_block
|
||||
|
||||
--- tag management
|
||||
local tags = {
|
||||
--- push new tags on top of the stack, from Anselme values
|
||||
push = function(self, state, val)
|
||||
|
|
@ -13,10 +15,12 @@ local tags = {
|
|||
for k, v in pairs(to_lua(val)) do new[k] = v end
|
||||
-- add
|
||||
table.insert(state.interpreter.tags, new)
|
||||
return self:len(state)
|
||||
end,
|
||||
--- same but do not merge with last stack item
|
||||
push_lua_no_merge = function(self, state, val)
|
||||
table.insert(state.interpreter.tags, val)
|
||||
return self:len(state)
|
||||
end,
|
||||
-- pop tag table on top of the stack
|
||||
pop = function(self, state)
|
||||
|
|
@ -31,6 +35,8 @@ local tags = {
|
|||
return #state.interpreter.tags
|
||||
end,
|
||||
--- pop item until we reached desired stack length
|
||||
-- try to prefer this to pop if possible, so in case we mess up the stack somehow it will restore the stack to a good state
|
||||
-- (we may allow tag push/pop from the user side at some point)
|
||||
trim = function(self, state, len)
|
||||
while #state.interpreter.tags > len do
|
||||
self:pop(state)
|
||||
|
|
@ -38,31 +44,120 @@ local tags = {
|
|||
end
|
||||
}
|
||||
|
||||
local function write_event(state, type, data)
|
||||
if state.interpreter.event_buffer and state.interpreter.event_type ~= type then
|
||||
error(("previous event of type %q has not been flushed, can't write new %q event"):format(state.interpreter.event_type, type))
|
||||
end
|
||||
if not state.interpreter.event_buffer then
|
||||
state.interpreter.event_type = type
|
||||
state.interpreter.event_buffer = {}
|
||||
end
|
||||
table.insert(state.interpreter.event_buffer, { data = data, tags = tags:current(state) })
|
||||
end
|
||||
--- event buffer management
|
||||
-- i.e. only for text and choice events
|
||||
local events = {
|
||||
--- add a new element to the event buffer
|
||||
-- will flush if needed
|
||||
-- returns true in case of success
|
||||
-- returns nil, err in case of error
|
||||
append = function(self, state, type, data)
|
||||
if state.interpreter.event_capture_stack[type] then
|
||||
local r, e = state.interpreter.event_capture_stack[type][#state.interpreter.event_capture_stack[type]](data)
|
||||
if not r then return r, e end
|
||||
else
|
||||
local r, e = self:make_space_for(state, type)
|
||||
if not r then return r, e end
|
||||
|
||||
local run_block
|
||||
if not state.interpreter.event_buffer then
|
||||
state.interpreter.event_type = type
|
||||
state.interpreter.event_buffer = {}
|
||||
end
|
||||
|
||||
table.insert(state.interpreter.event_buffer, data)
|
||||
end
|
||||
return true
|
||||
end,
|
||||
--- add a new item in the last element (a list of elements) of the event buffer
|
||||
-- will flush if needed
|
||||
-- will use default or a new list if buffer is empty
|
||||
-- returns true in case of success
|
||||
-- returns nil, err in case of error
|
||||
append_in_last = function(self, state, type, data, default)
|
||||
local r, e = self:make_space_for(state, type)
|
||||
if not r then return r, e end
|
||||
|
||||
if not state.interpreter.event_buffer then
|
||||
r, e = self:append(state, type, default or {})
|
||||
if not r then return r, e end
|
||||
end
|
||||
|
||||
table.insert(state.interpreter.event_buffer[#state.interpreter.event_buffer], data)
|
||||
|
||||
return true
|
||||
end,
|
||||
|
||||
--- start capturing events of a certain type
|
||||
-- when an event of the type is appended, fn will be called with this event data
|
||||
-- and the event will not be added to the event buffer
|
||||
-- fn returns nil, err in case of error
|
||||
push_capture = function(self, state, type, fn)
|
||||
if not state.interpreter.event_capture_stack[type] then
|
||||
state.interpreter.event_capture_stack[type] = {}
|
||||
end
|
||||
table.insert(state.interpreter.event_capture_stack[type], fn)
|
||||
end,
|
||||
--- stop capturing events of a certain type.
|
||||
-- must be called after a push_capture
|
||||
-- this is handled by a stack so nested capturing is allowed.
|
||||
pop_capture = function(self, state, type)
|
||||
table.remove(state.interpreter.event_capture_stack[type])
|
||||
if #state.interpreter.event_capture_stack[type] == 0 then
|
||||
state.interpreter.event_capture_stack[type] = nil
|
||||
end
|
||||
end,
|
||||
|
||||
-- flush event buffer if it's neccessary to push an event of the given type
|
||||
-- returns true in case of success
|
||||
-- returns nil, err in case of error
|
||||
make_space_for = function(self, state, type)
|
||||
if state.interpreter.event_buffer and state.interpreter.event_type ~= type and not state.interpreter.event_capture_stack[type] then
|
||||
return self:flush(state)
|
||||
end
|
||||
return true
|
||||
end,
|
||||
--- flush events and send them to the game if possible
|
||||
-- returns true in case of success
|
||||
-- returns nil, err in case of error
|
||||
flush = function(self, state)
|
||||
while state.interpreter.event_buffer do
|
||||
local type, buffer = state.interpreter.event_type, state.interpreter.event_buffer
|
||||
state.interpreter.event_type = nil
|
||||
state.interpreter.event_buffer = nil
|
||||
state.interpreter.skip_choices_until_flush = nil
|
||||
-- yield
|
||||
coroutine.yield(type, buffer)
|
||||
-- run choice
|
||||
if type == "choice" then
|
||||
local sel = state.interpreter.choice_selected
|
||||
state.interpreter.choice_selected = nil
|
||||
if not sel or sel < 1 or sel > #buffer then
|
||||
return nil, "invalid choice"
|
||||
else
|
||||
local choice = buffer[sel]._d
|
||||
-- execute in expected tag & event capture state
|
||||
local capture_state = state.interpreter.event_capture_stack
|
||||
state.interpreter.event_capture_stack = {}
|
||||
local i = tags:push_lua_no_merge(state, choice.tags)
|
||||
local _, e = run_block(state, choice.block)
|
||||
tags:trim(state, i-1)
|
||||
state.interpreter.event_capture_stack = capture_state
|
||||
if e then return nil, e end
|
||||
-- we discard return value from choice block as the execution is delayed until an event flush
|
||||
-- and we don't want to stop the execution of another function unexpectedly
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
}
|
||||
|
||||
-- returns var in case of success and there is a return
|
||||
-- return nil in case of success and there is no return
|
||||
-- return nil, err in case of error
|
||||
local function run_line(state, line)
|
||||
run_line = function(state, line)
|
||||
-- store line
|
||||
state.interpreter.running_line = line
|
||||
-- if line intend to push an event, flush buffer it it's a different event
|
||||
if line.push_event and state.interpreter.event_buffer and state.interpreter.event_type ~= line.push_event then
|
||||
local v, e = run_line(state, { source = line.source, type = "flush_events" })
|
||||
if e then return v, e end
|
||||
if v then return v end
|
||||
end
|
||||
-- line types
|
||||
if line.type == "condition" then
|
||||
line.parent_block.last_condition_success = nil
|
||||
|
|
@ -86,19 +181,27 @@ local function run_line(state, line)
|
|||
end
|
||||
end
|
||||
elseif line.type == "choice" then
|
||||
local t, er = eval_text(state, line.text)
|
||||
if not t then return t, er end
|
||||
table.insert(state.interpreter.choice_available, {
|
||||
tags = tags:current(state),
|
||||
block = line.child
|
||||
})
|
||||
write_event(state, "choice", t)
|
||||
local v, e = events:make_space_for(state, "choice")
|
||||
if not v then return v, ("%s; in automatic event flush at %s"):format(e, line.source) end
|
||||
local currentTags = tags:current(state)
|
||||
v, e = events:append(state, "choice", { _d = { tags = currentTags, block = line.child }}) -- new choice
|
||||
if not v then return v, e end
|
||||
events:push_capture(state, "text", function(event)
|
||||
local v2, e2 = events:append_in_last(state, "choice", event, { _d = { tags = currentTags, block = line.child }})
|
||||
if not v2 then return v2, e2 end
|
||||
end)
|
||||
v, e = eval_text_callback(state, line.text, function(text)
|
||||
local v2, e2 = events:append_in_last(state, "choice", { text = text, tags = currentTags }, { _d = { tags = currentTags, block = line.child }})
|
||||
if not v2 then return v2, e2 end
|
||||
end)
|
||||
events:pop_capture(state, "text")
|
||||
if not v then return v, ("%s; at %s"):format(e, line.source) end
|
||||
elseif line.type == "tag" then
|
||||
local v, e = eval(state, line.expression)
|
||||
if not v then return v, ("%s; at %s"):format(e, line.source) end
|
||||
tags:push(state, v)
|
||||
local i = tags:push(state, v)
|
||||
v, e = run_block(state, line.child)
|
||||
tags:pop(state)
|
||||
tags:trim(state, i-1)
|
||||
if e then return v, e end
|
||||
if v then return v end
|
||||
elseif line.type == "return" then
|
||||
|
|
@ -106,34 +209,18 @@ local function run_line(state, line)
|
|||
if not v then return v, ("%s; at %s"):format(e, line.source) end
|
||||
return v
|
||||
elseif line.type == "text" then
|
||||
local t, er = eval_text(state, line.text)
|
||||
if not t then return t, ("%s; at %s"):format(er, line.source) end
|
||||
write_event(state, "text", t)
|
||||
local v, e = events:make_space_for(state, "text") -- do this before any evaluation start
|
||||
if not v then return v, ("%s; in automatic event flush at %s"):format(e, line.source) end
|
||||
local currentTags = tags:current(state)
|
||||
v, e = eval_text_callback(state, line.text, function(text)
|
||||
-- why you would want to send a non-text event from there, I have no idea, but I'm not going to stop you
|
||||
local v2, e2 = events:append(state, "text", { text = text, tags = currentTags })
|
||||
if not v2 then return v2, e2 end
|
||||
end)
|
||||
if not v then return v, ("%s; at %s"):format(e, line.source) end
|
||||
elseif line.type == "flush_events" then
|
||||
while state.interpreter.event_buffer do
|
||||
local type, buffer = state.interpreter.event_type, state.interpreter.event_buffer
|
||||
state.interpreter.event_type = nil
|
||||
state.interpreter.event_buffer = nil
|
||||
-- yield
|
||||
coroutine.yield(type, buffer)
|
||||
-- run choice
|
||||
if type == "choice" then
|
||||
local sel = state.interpreter.choice_selected
|
||||
state.interpreter.choice_selected = nil
|
||||
if not sel or sel < 1 or sel > #state.interpreter.choice_available then
|
||||
return nil, "invalid choice"
|
||||
else
|
||||
local choice = state.interpreter.choice_available[sel]
|
||||
state.interpreter.choice_available = {}
|
||||
tags:push_lua_no_merge(state, choice.tags)
|
||||
local v, e = run_block(state, choice.block)
|
||||
tags:pop(state)
|
||||
if e then return v, e end
|
||||
-- discard return value from choice block as the execution is delayed until an event flush
|
||||
-- and we don't want to stop the execution of another function unexpectedly
|
||||
end
|
||||
end
|
||||
end
|
||||
local v, e = events:flush(state)
|
||||
if not v then return v, ("%s; in event flush at %s"):format(e, line.source) end
|
||||
elseif line.type == "checkpoint" then
|
||||
local reached, reachede = get_variable(state, line.namespace.."🏁")
|
||||
if not reached then return nil, reachede end
|
||||
|
|
@ -161,12 +248,8 @@ run_block = function(state, block, resume_from_there, i, j)
|
|||
local line = block[i]
|
||||
local skip = false
|
||||
-- skip current choice block if enabled
|
||||
if state.interpreter.skip_choices_until_flush then
|
||||
if line.type == "choice" then
|
||||
skip = true
|
||||
elseif line.type == "flush_events" or (line.push_event and line.push_event ~= "choice") then
|
||||
state.interpreter.skip_choices_until_flush = nil
|
||||
end
|
||||
if state.interpreter.skip_choices_until_flush and line.type == "choice" then
|
||||
skip = true
|
||||
end
|
||||
-- run line
|
||||
if not skip then
|
||||
|
|
@ -253,7 +336,7 @@ local function run(state, block, resume_from_there, i, j)
|
|||
-- run
|
||||
local v, e = run_block(state, block, resume_from_there, i, j)
|
||||
-- return to previous tag state
|
||||
-- tag stack pop when resuming is done when exiting the tag block
|
||||
-- when resuming is done, tag stack pop when exiting the tag block
|
||||
-- stray elements may be left on the stack if there is a return before we exit all the tag block, so we trim them
|
||||
if resume_from_there then
|
||||
tags:trim(state, tags_len)
|
||||
|
|
@ -280,7 +363,7 @@ local interpreter = {
|
|||
package.loaded[...] = interpreter
|
||||
eval = require((...):gsub("interpreter$", "expression"))
|
||||
local common = require((...):gsub("interpreter$", "common"))
|
||||
truthy, merge_state, to_lua, eval_text, get_variable = common.truthy, common.merge_state, common.to_lua, common.eval_text, common.get_variable
|
||||
truthy, merge_state, to_lua, get_variable, eval_text_callback = common.truthy, common.merge_state, common.to_lua, common.get_variable, common.eval_text_callback
|
||||
escape = require((...):gsub("interpreter%.interpreter$", "parser.common")).escape
|
||||
|
||||
return interpreter
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue