mirror of
https://github.com/Reuh/anselme.git
synced 2025-10-27 16:49:31 +00:00
Compare commits
5 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0aedf99a78 | |||
| 49c9741349 | |||
| 77c6ac6ba2 | |||
| 876135401c | |||
| ed7fe34853 |
34 changed files with 2085 additions and 137 deletions
|
|
@ -5,11 +5,70 @@ local Event = ast.abstract.Event
|
|||
|
||||
local operator_priority = require("anselme.common").operator_priority
|
||||
|
||||
--- ChoiceEventData represent the data returned by an event with the type `"choice"`.
|
||||
-- See the [language documentation](language.md#choices) for more details on how to create a choice event.
|
||||
--
|
||||
-- A ChoiceEventData contains a list of [LuaText](#luatext), each LuaText representing a separate choice of the choice event.
|
||||
--
|
||||
-- For example, the following Anselme script:
|
||||
--
|
||||
-- ```
|
||||
-- *| Yes!
|
||||
-- *| No.
|
||||
-- ```
|
||||
-- will return a choice event containing two LuaTexts, the first containing the text "Yes!" and the second "No.".
|
||||
--
|
||||
-- Usage:
|
||||
-- ```lua
|
||||
-- current_choice = nil
|
||||
-- waiting_for_choice = false
|
||||
--
|
||||
-- -- in your anselem event handling loop:
|
||||
-- if not waiting_for_choice then
|
||||
-- local event_type, event_data = run_state:step()
|
||||
-- if event_type == "choice" then
|
||||
-- -- event_data is a ChoiceEventData, i.e. a list of LuaText
|
||||
-- for i, luatext in ipairs(event_data) do
|
||||
-- write(("Choice number %s:"):format(i))
|
||||
-- -- luatext is a list of text parts { text = "text string", tags = { ... } }
|
||||
-- for _, textpart in ipairs(luatext) do
|
||||
-- write_choice_part_with_color(textpart.text, textpart.tags.color)
|
||||
-- end
|
||||
-- else
|
||||
-- -- handle other event types...
|
||||
-- end
|
||||
-- current_choice = event_data
|
||||
-- waiting_for_choice = true
|
||||
-- end
|
||||
-- end
|
||||
--
|
||||
-- -- somewhere in your code where choices are selected
|
||||
-- current_choice:select(choice_number)
|
||||
-- waiting_for_choice = false
|
||||
-- ```
|
||||
-- @title ChoiceEventData
|
||||
local ChoiceEventData = class {
|
||||
-- [1] = LuaText, ...
|
||||
|
||||
_selected = nil,
|
||||
|
||||
--- Choose the choice at position `choice` (number).
|
||||
--
|
||||
-- A choice must be selected after receiving a choice event and before calling `:step` again.
|
||||
choose = function(self, choice)
|
||||
self._selected = choice
|
||||
end
|
||||
end,
|
||||
|
||||
-- Returns a simple table representation of this TextEventData.
|
||||
-- This contains no metatable, method, or cycle; only a list of simple representation of LuaText (see LuaText:to_simple_table).
|
||||
-- { lua_text_1_simple, lua_text_2_simple, ... }
|
||||
to_simple_table = function(self)
|
||||
local t = {}
|
||||
for _, lua_text in ipairs(self) do
|
||||
table.insert(t, lua_text:to_simple_table())
|
||||
end
|
||||
return t
|
||||
end,
|
||||
}
|
||||
|
||||
local Choice
|
||||
|
|
@ -36,9 +95,9 @@ Choice = ast.abstract.Runtime(Event) {
|
|||
build_event_data = function(self, state, event_buffer)
|
||||
local l = ChoiceEventData:new()
|
||||
for _, c in event_buffer:iter(state) do
|
||||
table.insert(l, c.text)
|
||||
table.insert(l, c.text:to_lua(state))
|
||||
end
|
||||
return l
|
||||
return { l }
|
||||
end,
|
||||
post_flush_callback = function(self, state, event_buffer, data)
|
||||
local choice = data._selected
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ Struct = ast.abstract.Runtime {
|
|||
|
||||
to_lua = function(self, state)
|
||||
local l = {}
|
||||
for _, e in ipairs(self.table) do
|
||||
for _, e in pairs(self.table) do
|
||||
l[e[1]:to_lua(state)] = e[2]:to_lua(state)
|
||||
end
|
||||
return l
|
||||
|
|
|
|||
|
|
@ -5,16 +5,114 @@ local ArgumentTuple, Struct
|
|||
|
||||
local to_anselme = require("anselme.common.to_anselme")
|
||||
|
||||
local group_text_by_tag_identifier
|
||||
|
||||
--- A Lua-friendly representation of an Anselme Text value.
|
||||
-- They appear in both TextEventData and ChoiceEventData to represent the text that has to be shown.
|
||||
--
|
||||
-- It contains a list of _text parts_, which are parts of a single text, each part potentially having differrent tags attached.
|
||||
-- A text will typically only consist of a single part unless it was built using text interpolation.
|
||||
--
|
||||
-- Each text part is a table containing `text` (string) and `tags` (table) properties, for example: `{ text = "text part string", tags = { color = "red" } }`.
|
||||
-- @title LuaText
|
||||
-- @defer lua text
|
||||
local LuaText
|
||||
LuaText = class {
|
||||
-- [1] = { text = "string", tags = { tag_name = value, ... } }, ...
|
||||
|
||||
_state = nil,
|
||||
|
||||
--- Anselme Text value this was created from. For advanced usage only. See the source file [Text.lua](anselme/ast/Text.lua) for more information.
|
||||
-- @defer lua text
|
||||
raw = nil,
|
||||
|
||||
init = function(self, text, state)
|
||||
self._state = state
|
||||
self.raw = text
|
||||
for _, e in ipairs(text.list) do
|
||||
table.insert(self, { text = e[1]:to_lua(state), tags = e[2]:to_lua(state) })
|
||||
end
|
||||
end,
|
||||
|
||||
--- Returns a text representation of the LuaText, using Anselme's default formatting. Useful for debugging.
|
||||
--
|
||||
-- Usage: `print(luatext)`
|
||||
-- @defer lua text
|
||||
__tostring = function(self)
|
||||
return self.raw:format(self._state)
|
||||
end,
|
||||
|
||||
-- Returns a simple table representation of this LuaText.
|
||||
-- This contains no metatable, method, or cycle; only the list part of this LuaText.
|
||||
-- { text = "string", tags = { tag_name = value, ... } }
|
||||
to_simple_table = function(self)
|
||||
local t = {}
|
||||
for _, part in ipairs(self) do
|
||||
table.insert(t, part)
|
||||
end
|
||||
return t
|
||||
end,
|
||||
}
|
||||
|
||||
--- TextEventData represent the data returned by an event with the type `"text"`.
|
||||
-- See the [language documentation](language.md#texts) for more details on how to create a text event.
|
||||
--
|
||||
-- A TextEventData contains a list of [LuaText](#luatext), each LuaText representing a separate line of the text event.
|
||||
--
|
||||
-- For example, the following Anselme script:
|
||||
--
|
||||
-- ```
|
||||
-- | Hi!
|
||||
-- | My name's John.
|
||||
-- ```
|
||||
-- will return a text event containing two LuaTexts, the first containing the text "Hi!" and the second "My name's John.".
|
||||
--
|
||||
-- Usage:
|
||||
-- ```lua
|
||||
-- local event_type, event_data = run_state:step()
|
||||
-- if event_type == "text" then
|
||||
-- -- event_data is a TextEventData, i.e. a list of LuaText
|
||||
-- for _, luatext in ipairs(event_data) do
|
||||
-- -- luatext is a list of text parts { text = "text string", tags = { ... } }
|
||||
-- for _, textpart in ipairs(luatext) do
|
||||
-- write_text_part_with_color(textpart.text, textpart.tags.color)
|
||||
-- end
|
||||
-- write_text("\n") -- for example, if we want a newline between each text line
|
||||
-- end
|
||||
-- else
|
||||
-- -- handle other event types...
|
||||
-- end
|
||||
-- ```
|
||||
-- @title TextEventData
|
||||
local TextEventData
|
||||
TextEventData = class {
|
||||
-- returns a list of TextEventData where the first element of each text of each TextEventData has the same value for the tag tag_name
|
||||
group_by = function(self, tag_name)
|
||||
-- [1] = LuaText, ...
|
||||
|
||||
--- Returns a list of TextEventData where the first part of each LuaText of each TextEventData has the same value for the tag `tag_key`.
|
||||
--
|
||||
-- In other words, this groups all the LuaTexts contained in this TextEventData using the `tag_key` tag and returns a list containing these groups.
|
||||
--
|
||||
-- For example, with the following Anselme script:
|
||||
-- ```
|
||||
-- speaker: "John" #
|
||||
-- | A
|
||||
-- | B
|
||||
-- speaker: "Lana" #
|
||||
-- | C
|
||||
-- speaker: "John" #
|
||||
-- | D
|
||||
-- ```
|
||||
-- calling `text_event_data:group_by("speaker")` will return a list of three TextEventData:
|
||||
-- * the first with the texts "A" and "B"; both with the tag `speaker="John"`
|
||||
-- * the second with the text "C"; with the tag `speaker="Lana"`
|
||||
-- * the last with the text "D"; wiith the tag `speaker="John"`
|
||||
group_by = function(self, tag_key)
|
||||
if type(tag_key) == "string" then tag_key = to_anselme(tag_key) end
|
||||
local l = {}
|
||||
local current_group
|
||||
local tag_key = to_anselme(tag_name)
|
||||
local last_value
|
||||
for _, event in ipairs(self) do
|
||||
local list = event.list
|
||||
for _, luatext in ipairs(self) do
|
||||
local list = luatext.raw.list
|
||||
if #list > 0 then
|
||||
local value = list[1][2]:get_strict(tag_key)
|
||||
if (not current_group) or (last_value == nil and value) or (last_value and value == nil) or (last_value and value and last_value:hash() ~= value:hash()) then -- new group
|
||||
|
|
@ -22,11 +120,22 @@ TextEventData = class {
|
|||
table.insert(l, current_group)
|
||||
last_value = value
|
||||
end
|
||||
table.insert(current_group, event) -- add to current group
|
||||
table.insert(current_group, luatext) -- add to current group
|
||||
end
|
||||
end
|
||||
return l
|
||||
end,
|
||||
|
||||
-- Returns a simple table representation of this TextEventData.
|
||||
-- This contains no metatable, method, or cycle; only a list of simple representation of LuaText (see LuaText:to_simple_table).
|
||||
-- { lua_text_1_simple, lua_text_2_simple, ... }
|
||||
to_simple_table = function(self)
|
||||
local t = {}
|
||||
for _, lua_text in ipairs(self) do
|
||||
table.insert(t, lua_text:to_simple_table())
|
||||
end
|
||||
return t
|
||||
end,
|
||||
}
|
||||
|
||||
local Text
|
||||
|
|
@ -68,6 +177,10 @@ Text = Runtime(Event) {
|
|||
return ("| %s |"):format(table.concat(t, " "))
|
||||
end,
|
||||
|
||||
to_lua = function(self, state)
|
||||
return LuaText:new(self, state)
|
||||
end,
|
||||
|
||||
-- autocall when used directly as a statement
|
||||
eval_statement = function(self, state)
|
||||
return self:call(state, ArgumentTuple:new())
|
||||
|
|
@ -77,14 +190,20 @@ Text = Runtime(Event) {
|
|||
|
||||
build_event_data = function(self, state, event_buffer)
|
||||
local l = TextEventData:new()
|
||||
for _, event in event_buffer:iter(state) do
|
||||
table.insert(l, event)
|
||||
for _, text in event_buffer:iter(state) do
|
||||
table.insert(l, text:to_lua(state))
|
||||
end
|
||||
if state.scope:defined(group_text_by_tag_identifier) then
|
||||
local tag_key = state.scope:get(group_text_by_tag_identifier)
|
||||
return l:group_by(tag_key)
|
||||
else
|
||||
return { l }
|
||||
end
|
||||
return l
|
||||
end,
|
||||
}
|
||||
|
||||
package.loaded[...] = Text
|
||||
ArgumentTuple, Struct = ast.ArgumentTuple, ast.Struct
|
||||
group_text_by_tag_identifier = ast.Identifier:new("_group_text_by_tag")
|
||||
|
||||
return Text
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ return ast.abstract.Node {
|
|||
type = "event",
|
||||
init = false,
|
||||
|
||||
-- returns value that will be yielded by the whole event buffer data on flush
|
||||
-- returns list of values that will each be yielded in order by the whole event buffer data on flush
|
||||
build_event_data = function(self, state, event_buffer)
|
||||
error("build_event_data not implemented for "..self.type)
|
||||
end,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
local class = require("anselme.lib.class")
|
||||
local fmt = require("anselme.common").fmt
|
||||
local binser = require("anselme.lib.binser")
|
||||
local utf8 = utf8 or require("lua-utf8")
|
||||
local utf8 = utf8 or (love and require("utf8") or require("lua-utf8"))
|
||||
local unpack = table.unpack or unpack
|
||||
|
||||
-- NODES SHOULD BE IMMUTABLE AFTER CREATION IF POSSIBLE!
|
||||
|
|
|
|||
|
|
@ -13,31 +13,26 @@
|
|||
-- local state = anselme.new()
|
||||
-- state:load_stdlib()
|
||||
--
|
||||
-- -- read an anselme script file
|
||||
-- local f = assert(io.open("script.ans"))
|
||||
-- local script = anselme.parse(f:read("a"), "script.ans")
|
||||
-- f:close()
|
||||
--
|
||||
-- -- load the script in a new branch
|
||||
-- -- load an anselme script file in a new branch
|
||||
-- local run_state = state:branch()
|
||||
-- run_state:run(script)
|
||||
-- run_state:run_file("script.ans")
|
||||
--
|
||||
-- -- run the script
|
||||
-- while run_state:active() do
|
||||
-- local e, data = run_state:step()
|
||||
-- if e == "text" then
|
||||
-- local event, data = run_state:step()
|
||||
-- if event == "text" then
|
||||
-- for _, l in ipairs(data) do
|
||||
-- print(l:format(run_state))
|
||||
-- print(l)
|
||||
-- end
|
||||
-- elseif e == "choice" then
|
||||
-- elseif event == "choice" then
|
||||
-- for i, l in ipairs(data) do
|
||||
-- print(("%s> %s"):format(i, l:format(run_state)))
|
||||
-- print(("%s> %s"):format(i, l))
|
||||
-- end
|
||||
-- local choice = tonumber(io.read("l"))
|
||||
-- data:choose(choice)
|
||||
-- elseif e == "return" then
|
||||
-- elseif event == "return" then
|
||||
-- run_state:merge()
|
||||
-- elseif e == "error" then
|
||||
-- elseif event == "error" then
|
||||
-- error(data)
|
||||
-- end
|
||||
-- end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- TODO: upstream
|
||||
-- NOTE: Modified to serialize function upvalues.
|
||||
|
||||
-- binser.lua
|
||||
|
||||
|
|
|
|||
394
anselme/lib/json.lua
Normal file
394
anselme/lib/json.lua
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
-- NOTE: modified to add a json.null constant to allow encoding/decoding null values
|
||||
|
||||
--
|
||||
-- json.lua
|
||||
--
|
||||
-- Copyright (c) 2020 rxi
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
-- this software and associated documentation files (the "Software"), to deal in
|
||||
-- the Software without restriction, including without limitation the rights to
|
||||
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
-- of the Software, and to permit persons to whom the Software is furnished to do
|
||||
-- so, subject to the following conditions:
|
||||
--
|
||||
-- The above copyright notice and this permission notice shall be included in all
|
||||
-- copies or substantial portions of the Software.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
-- SOFTWARE.
|
||||
--
|
||||
|
||||
local json = { _version = "0.1.2" }
|
||||
|
||||
json.null = {"json.null"}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Encode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local encode
|
||||
|
||||
local escape_char_map = {
|
||||
[ "\\" ] = "\\",
|
||||
[ "\"" ] = "\"",
|
||||
[ "\b" ] = "b",
|
||||
[ "\f" ] = "f",
|
||||
[ "\n" ] = "n",
|
||||
[ "\r" ] = "r",
|
||||
[ "\t" ] = "t",
|
||||
}
|
||||
|
||||
local escape_char_map_inv = { [ "/" ] = "/" }
|
||||
for k, v in pairs(escape_char_map) do
|
||||
escape_char_map_inv[v] = k
|
||||
end
|
||||
|
||||
|
||||
local function escape_char(c)
|
||||
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
|
||||
end
|
||||
|
||||
|
||||
local function encode_nil(val)
|
||||
return "null"
|
||||
end
|
||||
|
||||
|
||||
local function encode_table(val, stack)
|
||||
if val == json.null then return "null" end
|
||||
|
||||
local res = {}
|
||||
stack = stack or {}
|
||||
|
||||
-- Circular reference?
|
||||
if stack[val] then error("circular reference") end
|
||||
|
||||
stack[val] = true
|
||||
|
||||
if rawget(val, 1) ~= nil or next(val) == nil then
|
||||
-- Treat as array -- check keys are valid and it is not sparse
|
||||
local n = 0
|
||||
for k in pairs(val) do
|
||||
if type(k) ~= "number" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
if n ~= #val then
|
||||
error("invalid table: sparse array")
|
||||
end
|
||||
-- Encode
|
||||
for i, v in ipairs(val) do
|
||||
table.insert(res, encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "[" .. table.concat(res, ",") .. "]"
|
||||
|
||||
else
|
||||
-- Treat as an object
|
||||
for k, v in pairs(val) do
|
||||
if type(k) ~= "string" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "{" .. table.concat(res, ",") .. "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function encode_string(val)
|
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||
end
|
||||
|
||||
|
||||
local function encode_number(val)
|
||||
-- Check for NaN, -inf and inf
|
||||
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||
error("unexpected number value '" .. tostring(val) .. "'")
|
||||
end
|
||||
return string.format("%.14g", val)
|
||||
end
|
||||
|
||||
|
||||
local type_func_map = {
|
||||
[ "nil" ] = encode_nil,
|
||||
[ "table" ] = encode_table,
|
||||
[ "string" ] = encode_string,
|
||||
[ "number" ] = encode_number,
|
||||
[ "boolean" ] = tostring,
|
||||
}
|
||||
|
||||
|
||||
encode = function(val, stack)
|
||||
local t = type(val)
|
||||
local f = type_func_map[t]
|
||||
if f then
|
||||
return f(val, stack)
|
||||
end
|
||||
error("unexpected type '" .. t .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.encode(val)
|
||||
return ( encode(val) )
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Decode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local parse
|
||||
|
||||
local function create_set(...)
|
||||
local res = {}
|
||||
for i = 1, select("#", ...) do
|
||||
res[ select(i, ...) ] = true
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||
local literals = create_set("true", "false", "null")
|
||||
|
||||
local literal_map = {
|
||||
[ "true" ] = true,
|
||||
[ "false" ] = false,
|
||||
[ "null" ] = json.null,
|
||||
}
|
||||
|
||||
|
||||
local function next_char(str, idx, set, negate)
|
||||
for i = idx, #str do
|
||||
if set[str:sub(i, i)] ~= negate then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #str + 1
|
||||
end
|
||||
|
||||
|
||||
local function decode_error(str, idx, msg)
|
||||
local line_count = 1
|
||||
local col_count = 1
|
||||
for i = 1, idx - 1 do
|
||||
col_count = col_count + 1
|
||||
if str:sub(i, i) == "\n" then
|
||||
line_count = line_count + 1
|
||||
col_count = 1
|
||||
end
|
||||
end
|
||||
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||
end
|
||||
|
||||
|
||||
local function codepoint_to_utf8(n)
|
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||
local f = math.floor
|
||||
if n <= 0x7f then
|
||||
return string.char(n)
|
||||
elseif n <= 0x7ff then
|
||||
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||
elseif n <= 0xffff then
|
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
elseif n <= 0x10ffff then
|
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
end
|
||||
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||
end
|
||||
|
||||
|
||||
local function parse_unicode_escape(s)
|
||||
local n1 = tonumber( s:sub(1, 4), 16 )
|
||||
local n2 = tonumber( s:sub(7, 10), 16 )
|
||||
-- Surrogate pair?
|
||||
if n2 then
|
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||
else
|
||||
return codepoint_to_utf8(n1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function parse_string(str, i)
|
||||
local res = ""
|
||||
local j = i + 1
|
||||
local k = j
|
||||
|
||||
while j <= #str do
|
||||
local x = str:byte(j)
|
||||
|
||||
if x < 32 then
|
||||
decode_error(str, j, "control character in string")
|
||||
|
||||
elseif x == 92 then -- `\`: Escape
|
||||
res = res .. str:sub(k, j - 1)
|
||||
j = j + 1
|
||||
local c = str:sub(j, j)
|
||||
if c == "u" then
|
||||
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
|
||||
or str:match("^%x%x%x%x", j + 1)
|
||||
or decode_error(str, j - 1, "invalid unicode escape in string")
|
||||
res = res .. parse_unicode_escape(hex)
|
||||
j = j + #hex
|
||||
else
|
||||
if not escape_chars[c] then
|
||||
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
|
||||
end
|
||||
res = res .. escape_char_map_inv[c]
|
||||
end
|
||||
k = j + 1
|
||||
|
||||
elseif x == 34 then -- `"`: End of string
|
||||
res = res .. str:sub(k, j - 1)
|
||||
return res, j + 1
|
||||
end
|
||||
|
||||
j = j + 1
|
||||
end
|
||||
|
||||
decode_error(str, i, "expected closing quote for string")
|
||||
end
|
||||
|
||||
|
||||
local function parse_number(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local s = str:sub(i, x - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||
end
|
||||
return n, x
|
||||
end
|
||||
|
||||
|
||||
local function parse_literal(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local word = str:sub(i, x - 1)
|
||||
if not literals[word] then
|
||||
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||
end
|
||||
return literal_map[word], x
|
||||
end
|
||||
|
||||
|
||||
local function parse_array(str, i)
|
||||
local res = {}
|
||||
local n = 1
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local x
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of array?
|
||||
if str:sub(i, i) == "]" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read token
|
||||
x, i = parse(str, i)
|
||||
res[n] = x
|
||||
n = n + 1
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "]" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local function parse_object(str, i)
|
||||
local res = {}
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local key, val
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of object?
|
||||
if str:sub(i, i) == "}" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read key
|
||||
if str:sub(i, i) ~= '"' then
|
||||
decode_error(str, i, "expected string for key")
|
||||
end
|
||||
key, i = parse(str, i)
|
||||
-- Read ':' delimiter
|
||||
i = next_char(str, i, space_chars, true)
|
||||
if str:sub(i, i) ~= ":" then
|
||||
decode_error(str, i, "expected ':' after key")
|
||||
end
|
||||
i = next_char(str, i + 1, space_chars, true)
|
||||
-- Read value
|
||||
val, i = parse(str, i)
|
||||
-- Set
|
||||
res[key] = val
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "}" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local char_func_map = {
|
||||
[ '"' ] = parse_string,
|
||||
[ "0" ] = parse_number,
|
||||
[ "1" ] = parse_number,
|
||||
[ "2" ] = parse_number,
|
||||
[ "3" ] = parse_number,
|
||||
[ "4" ] = parse_number,
|
||||
[ "5" ] = parse_number,
|
||||
[ "6" ] = parse_number,
|
||||
[ "7" ] = parse_number,
|
||||
[ "8" ] = parse_number,
|
||||
[ "9" ] = parse_number,
|
||||
[ "-" ] = parse_number,
|
||||
[ "t" ] = parse_literal,
|
||||
[ "f" ] = parse_literal,
|
||||
[ "n" ] = parse_literal,
|
||||
[ "[" ] = parse_array,
|
||||
[ "{" ] = parse_object,
|
||||
}
|
||||
|
||||
|
||||
parse = function(str, idx)
|
||||
local chr = str:sub(idx, idx)
|
||||
local f = char_func_map[chr]
|
||||
if f then
|
||||
return f(str, idx)
|
||||
end
|
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
local res, idx = parse(str, next_char(str, 1, space_chars, true))
|
||||
idx = next_char(str, idx, space_chars, true)
|
||||
if idx <= #str then
|
||||
decode_error(str, idx, "trailing garbage")
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
return json
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
local class = require("anselme.lib.class")
|
||||
local utf8 = utf8 or require("lua-utf8")
|
||||
local utf8 = utf8 or (love and require("utf8") or require("lua-utf8"))
|
||||
|
||||
local Source
|
||||
Source = class {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
local expression_to_ast = require("anselme.parser.expression.to_ast")
|
||||
|
||||
local utf8 = utf8 or (love and require("utf8") or require("lua-utf8"))
|
||||
local ast = require("anselme.ast")
|
||||
local PartialScope, Block, Call, Identifier = ast.PartialScope, ast.Block, ast.Call, ast.Identifier
|
||||
|
||||
|
|
|
|||
184
anselme/server/Client.lua
Normal file
184
anselme/server/Client.lua
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
--- This is a Lua implementation of an Anselme client, with a nice API that mirrors the Anselme [State API](api.md#state) to communicate with the server.
|
||||
--
|
||||
-- Usage: create a Client object using the functions in the [anselme.server module](#anselme_server) and call `server:process()` regularly to process messages from the Server.
|
||||
--
|
||||
-- The API available here tries to follow the [State API](api.md#state) as much as possible, with the following differences:
|
||||
-- * functions that return a value in State take an additionnal argument `callback`:
|
||||
-- * if it is a function `callback(ret1, ret2, ...)`, it is called as soon as the return values `ret1, ret2, ...` are received. The function also returns the identifier `call_id` associated with the callback (to optionally cancel the callback later using `client:cancel(call_id)`).
|
||||
-- * if it is `nil`, return values are discarded;
|
||||
-- * if it is the string `"block"`, the call will block until the return values are received. The function returns these values directly.
|
||||
-- * functions that returns a `State` in State now returns a `Client`;
|
||||
-- * return values are converted to a simpler representation if possible (no metamethods, userdata or cycles) to make serialization simpler - in particular, Anselme values are automatically converted to Lua primitives.
|
||||
-- * a few new methods are introduced, see below.
|
||||
--
|
||||
-- Implementing a Client in other languages should be relatively easy: if your client language has a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) library, point it to the Anselme server you started using [`server.new_json_rpc_server()`](#new_json_rpc_server) and you're done.
|
||||
-- You should then be able to call any of the methods described in the [Server](#server).
|
||||
-- Additionnaly, if you plan to use the `define_rpc` or `define_local_rpc` server methods, you will need to implement the following remote method in your client that will be called by the server:
|
||||
-- * `call(function_id, ...)` where `function_id` (string) is the function identifier that was given when `define_rpc` or `define_local_rpc` was called, and `...` is a list of arguments. This must call the function associated with the `function_id` using the given arguments, and returns the values returned by the call (as a list of return values: `{ret1, ret2, ...}`).
|
||||
|
||||
local class = require("anselme.lib.class")
|
||||
local common = require("anselme.common")
|
||||
local uuid = common.uuid
|
||||
|
||||
local Client
|
||||
Client = class {
|
||||
rpc = nil,
|
||||
rpc_functions = nil,
|
||||
|
||||
-- `rpc` is the Rpc object to use to communicate with the Anselme server
|
||||
init = function(self, rpc, branch_from, branch_id)
|
||||
-- create a new branch from an existing server
|
||||
if branch_from then
|
||||
self.branch_id = branch_id
|
||||
self.source_branch = branch_from
|
||||
self.rpc = branch_from.rpc
|
||||
self.rpc_functions = branch_from.rpc_functions
|
||||
-- create new empty server
|
||||
else
|
||||
self.rpc_functions = {}
|
||||
self.rpc = rpc
|
||||
self.rpc.methods.call = function(func_id, ...)
|
||||
local fn = assert(self.rpc_functions[func_id])
|
||||
return fn(...)
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
--- Process received messages.
|
||||
--
|
||||
-- Must be called regularly.
|
||||
-- If `block` is true, the function is allowed to block execution until a message is received.
|
||||
process = function(self, block)
|
||||
self.rpc:process(block)
|
||||
end,
|
||||
|
||||
--- Cancel the callback associated with the call `call_id`.
|
||||
-- This does not stop the remote method execution; only prevent the callback from being called.
|
||||
cancel = function(self, call_id)
|
||||
self.rpc:cancel(call_id)
|
||||
end,
|
||||
|
||||
--- If the last event was a `choice`, choose the `i`-th choice.
|
||||
-- This must be called before calling `:step` again after receiving a choice event.
|
||||
choose = function(self, i)
|
||||
self.rpc:call("choose", { self.branch_id, i })
|
||||
end,
|
||||
--- Remove the branch from the server.
|
||||
-- The branch (and therefore this Client branch) can't be used after calling this method.
|
||||
remove = function(self)
|
||||
self.rpc:call("remove", { self.branch_id })
|
||||
end,
|
||||
|
||||
--- Defines a function in the global scope, that calls the Lua function `func` on the Client when called.
|
||||
--
|
||||
-- The function will not be sent to the server; it will be directly executed on the client (i.e. your game code)
|
||||
-- each time a script on the server needs it to be called.
|
||||
--
|
||||
-- Usage: `client:define_rpc("teleport", "(position)", function(position) player:teleport(position) end)`
|
||||
define_rpc = function(self, name, args, func)
|
||||
local func_id = uuid()
|
||||
self.rpc_functions[func_id] = func
|
||||
self.rpc:call("define_rpc", { self.branch_id, name, args, func_id })
|
||||
return func_id
|
||||
end,
|
||||
--- Same as `:define_rpc`, but define the function in the current scope.
|
||||
define_local_rpc = function(self, name, args, func)
|
||||
local func_id = uuid()
|
||||
self.rpc_functions[func_id] = func
|
||||
self.rpc:call("define_local_rpc", { self.branch_id, name, args, func_id })
|
||||
return args
|
||||
end,
|
||||
|
||||
--- ## Methods and fields that mirror the State API
|
||||
|
||||
--- Same as [`state:load_stdlib(language)`](api.md#load_stdlib-language).
|
||||
load_stdlib = function(self, language)
|
||||
self.rpc:call("load_stdlib", { self.branch_id, language })
|
||||
end,
|
||||
|
||||
--- Same as [`state.branch_id`](api.md#branch_id).
|
||||
branch_id = "main",
|
||||
--- Same as [`state.source_branch`](api.md#source_branch), but refers to the source `Client` instead of a `State`.
|
||||
source_branch = nil,
|
||||
--- Same as [`state:branch(branch_id)`](api.md#branch-branch_id), but returns a new `Client` instead of a `State`.
|
||||
branch = function(self, branch_id, callback)
|
||||
local branch_id
|
||||
if callback == "block" then
|
||||
return Client:new(self.rpc, self, self.rpc:call("branch", { self.branch_id, branch_id }, callback))
|
||||
else
|
||||
return self.rpc:call("branch", { self.branch_id, branch_id }, function(id) callback(Client:new(self.rpc, self, id)) end)
|
||||
end
|
||||
end,
|
||||
--- Same as [`state:merge()`](api.md#merge).
|
||||
merge = function(self)
|
||||
self.rpc:call("merge", { self.branch_id })
|
||||
end,
|
||||
|
||||
--- Same as [`state:define(name, value, func, raw_mode)`](api.md#api.md#define-name-value-func-raw_mode), but if `func_code` is given, it must be a string containing the function code.
|
||||
--
|
||||
-- Note that the given code will be executed on the server, and that there is no sandboxing of any kind;
|
||||
--
|
||||
-- Example: `client:define("main", "print", "(message::is string)", "function(message) print(message) end")`.
|
||||
define = function(self, name, value, func_code, raw_mode)
|
||||
self.rpc:call("define", { self.branch_id, name, value, func_code, raw_mode })
|
||||
end,
|
||||
--- Same as [`define`](#define-name-value-func_code-raw_mode), but calls [`state:define_local(name, value, func, raw_mode)`](api.md#api.md#define_local-name-value-func-raw_mode).
|
||||
define_local = function(self, name, value, func_code, raw_mode)
|
||||
self.rpc:call("define_local", { self.branch_id, name, value, func_code, raw_mode })
|
||||
end,
|
||||
--- Same as [`state:defined(name)`](api.md#defined-name).
|
||||
defined = function(self, name, callback)
|
||||
return self.rpc:call("defined", { self.branch_id, name }, callback)
|
||||
end,
|
||||
--- Same as [`state:defined_local(name)`](api.md#defined_local-name).
|
||||
defined_local = function(self, name, callback)
|
||||
return self.rpc:call("defined_local", { self.branch_id, name }, callback)
|
||||
end,
|
||||
|
||||
--- Same as [`state:save()`](api.md#save).
|
||||
save = function(self, callback)
|
||||
return self.rpc:call("save", { self.branch_id }, callback)
|
||||
end,
|
||||
--- Same as [`state:load(save)`](api.md#load-save).
|
||||
load = function(self, save)
|
||||
self.rpc:call("load", { self.branch_id, save })
|
||||
end,
|
||||
|
||||
--- Same as [`state:active()`](api.md#active).
|
||||
active = function(self, callback)
|
||||
return self.rpc:call("active", { self.branch_id }, callback)
|
||||
end,
|
||||
--- Same as [`state:state()`](api.md#state).
|
||||
state = function(self, callback)
|
||||
return self.rpc:call("state", { self.branch_id }, callback)
|
||||
end,
|
||||
--- Same as [`state:run(code, source, tags)`](api.md#run-code-source-tags).
|
||||
run = function(self, code, source, tags)
|
||||
self.rpc:call("run", { self.branch_id, code, source, tags })
|
||||
end,
|
||||
--- Same as [`state:run_file(code, source, tags)`](api.md#run_file-code-source-tags).
|
||||
run_file = function(self, path, tags)
|
||||
self.rpc:call("run_file", { self.branch_id, path, tags })
|
||||
end,
|
||||
--- Same as [`state:step)`](api.md#step), but returns:
|
||||
-- * for `text` and `choice` events, a list of lines `{ { { text = "line 1 part 2", tags = { ... } }, ... }, ... }` (in other word, [`TextEventData`](api.md#texteventdata) and [`ChoiceEventData`](api.md#choiceeventdata) stripped of everything but their list of text parts);
|
||||
-- * for `return` events, the return value converted to Lua primitives;
|
||||
-- * for other events, it will try to return the event data as-is.
|
||||
step = function(self, callback)
|
||||
return self.rpc:call("step", { self.branch_id }, callback)
|
||||
end,
|
||||
--- Same as [`state:interrupt(code, source, tags)`](api.md#interrupt-code-source-tags).
|
||||
interrupt = function(self, code, source, tags)
|
||||
self.rpc:call("interrupt", { self.branch_id, code, source, tags })
|
||||
end,
|
||||
--- Same as [`state:eval(code, source, tags)`](api.md#eval-code-source-tags), but the returned value is converted to Lua primitives.
|
||||
eval = function(self, code, source, tags, callback)
|
||||
return self.rpc:call("eval", { self.branch_id, code, source, tags }, callback)
|
||||
end,
|
||||
--- Same as [`state:eval_local(code, source, tags)`](api.md#eval_local-code-source-tags), but the returned value is converted to Lua primitives.
|
||||
eval_local = function(self, code, source, tags, callback)
|
||||
return self.rpc:call("eval", { self.branch_id, code, source, tags }, callback)
|
||||
end,
|
||||
}
|
||||
|
||||
return Client
|
||||
165
anselme/server/Server.lua
Normal file
165
anselme/server/Server.lua
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
--- An Anselme server instance.
|
||||
--
|
||||
-- Usage: create a Server object using the functions in the [anselme.server module](#anselme_server) and call `server:process()` regularly to process messages from the Client.
|
||||
--
|
||||
-- If you are implementing your own client, the following methods are available to be remotely called by your client:
|
||||
-- * Note:
|
||||
-- * in all the following methods, the first parameter `branch_id` (string) is the id of the Anselme branch to operate on;
|
||||
-- * methods that return something always returns a list of return values: `{ ret1, ret2, ... }`.
|
||||
-- * `choose(branch_id, i)`: if the last event was a `choice`, choose the `i`-th (number) line in the choice list;
|
||||
-- * `remove(branch_id)`: removes the branch from the server; no further operation will be possible on the branch;
|
||||
-- * `load_stdlib(branch_id, language)`: calls [`state:load_stdlib(language)`](api.md#load_stdlib-language) on the branch;
|
||||
-- * `branch(branch_id[, new_branch_id])`: calls [`state:branch(branch_id)`](api.md#branch-branch_id) on the branch; returns the id of the new branch (string);
|
||||
-- * `merge(branch_id)`: calls [`state:merge()`](api.md#merge) on the branch;
|
||||
-- * `define(branch_id, name, args, func_code, raw_mode)`: calls [`state:define(branch_id, name, args, func, raw_mode)`](api.md#define-name-value-func-raw_mode) on the branch; if `func_code` is given, `func` will be a function generated from the Lua code `func_code` (string, example: `define("main", "print", "(message::is string)", "function(message) print(message) end")`). Note that whatever is in `func_code` will be executed on the server, and that there is no sandboxing of any kind;
|
||||
-- * `define_rpc(branch_id, name, args, func_id)`: defines a function in the branch that, when called, will call the remote method `call(func_id, ...)` on the client and block until it returns. In other words, this allows the Anselme script running on the server to transparently call the function that is associated with the id `func_id` on the client.
|
||||
-- * `define_local(branch_id, name, args, func_code, raw_mode)`: same as `define`, but calls [`state:define_local(branch_id, name, args, func, raw_mode)`](api.md#define_local-name-value-func-raw_mode);
|
||||
-- * `define_local_rpc(branch_id, name, args, func_id)`: same as `define_rpc`, but defines the function in the current scope;
|
||||
-- * `defined(branch_id, name)`: calls [`state:defined(name)`](api.md#defined-name) on the branch and returns its result;
|
||||
-- * `defined_local(branch_id, name)`: calls [`state:defined_local(name)`](api.md#defined_local-name) on the branch and returns its result;
|
||||
-- * `save(branch_id)`: calls [`state:save()`](api.md#save) on the branch and returns its result;
|
||||
-- * `load(branch_id, save)`: calls [`state:load(save)`](api.md#load-save) on the branch;
|
||||
-- * `active(branch_id)`: calls [`state:active()`](api.md#active) on the branch and returns its result;
|
||||
-- * `state(branch_id)`: calls [`state:state()`](api.md#state) on the branch and returns its result;
|
||||
-- * `run(branch_id, code, source, tags)`: calls [`state:run(code, source, tags)`](api.md#run-code-source-tags) on the branch;
|
||||
-- * `run_file(branch_id, path, tags)`: calls [`state:run_file(path, tags)`](api.md#run_file-path-tags) on the branch;
|
||||
-- * `step(branch_id)`: calls [`state:step()`](api.md#step) on the branch and returns:
|
||||
-- * for `text` and `choices` events, a list of lines `{ { { text = "line 1 part 2", tags = { ... } }, ... }, ... }` (in other word, [`TextEventData`](api.md#texteventdata) and [`ChoiceEventData`](api.md#choiceeventdata) stripped of everything but their list of text parts);
|
||||
-- * for `return` events, the return value converted to Lua;
|
||||
-- * for other events, it will try to return the event data as-is.
|
||||
-- * `interrupt(branch_id, code, source, tags)`: calls [`state:interrupt(code, source, tags)`](api.md#interrupt-code-source-tags) on the branch;
|
||||
-- * `eval(branch_id, code, source, tags)`: calls [`state:eval(code, source, tags)`](api.md#eval-code-source-tags) on the branch and returns its result, converted to Lua;
|
||||
-- * `eval_local(branch_id, code, source, tags)`: calls [`state:eval_local(code, source, tags)`](api.md#eval_local-code-source-tags) on the branch and returns its result, converted to Lua.
|
||||
|
||||
local class = require("anselme.lib.class")
|
||||
local anselme = require("anselme")
|
||||
|
||||
local Server
|
||||
Server = class {
|
||||
rpc = nil,
|
||||
branches = nil,
|
||||
|
||||
-- `rpc` is the Rpc object to use to communicate with the Anselme client
|
||||
init = function(self, rpc)
|
||||
local branches = {
|
||||
main = {
|
||||
choice = nil,
|
||||
state = anselme.new()
|
||||
}
|
||||
}
|
||||
|
||||
local methods = {
|
||||
choose = function(branch, i)
|
||||
branch.choice:choose(i)
|
||||
end,
|
||||
remove = function(branch)
|
||||
branches[branch.state.branch_id] = nil
|
||||
end,
|
||||
|
||||
load_stdlib = function(branch, language)
|
||||
branch.state:load_stdlib(language)
|
||||
end,
|
||||
|
||||
branch = function(branch, new_branch_id)
|
||||
local new_branch = branch.state:branch(new_branch_id)
|
||||
branches[new_branch.branch_id] = {
|
||||
choice = nil,
|
||||
state = new_branch
|
||||
}
|
||||
return new_branch.branch_id
|
||||
end,
|
||||
merge = function(branch)
|
||||
branch.state:merge()
|
||||
end,
|
||||
|
||||
define = function(branch, name, args, func_code, raw_mode)
|
||||
if func_code then func_code = assert(load("return "..func_code))() end
|
||||
branch.state:define(name, args, func_code, raw_mode)
|
||||
end,
|
||||
define_rpc = function(branch, name, args, func_id)
|
||||
branch.state:define(name, args, function(...)
|
||||
return rpc:call("call", { func_id, ... }, "block")
|
||||
end)
|
||||
end,
|
||||
define_local = function(branch, name, args, func_code, raw_mode)
|
||||
if func_code then func_code = assert(load("return "..func_code))() end
|
||||
branch.state:define_local(name, args, func_code, raw_mode)
|
||||
end,
|
||||
define_local_rpc = function(branch, name, args, func_id)
|
||||
branch.state:define_local(name, args, function(...)
|
||||
return rpc:call("call", { func_id, ... }, "block")
|
||||
end)
|
||||
end,
|
||||
defined = function(branch, name)
|
||||
return branch.state:defined(name)
|
||||
end,
|
||||
defined_local = function(branch, name)
|
||||
return branch.state:defined_local(name)
|
||||
end,
|
||||
|
||||
save = function(branch)
|
||||
return branch.state:save()
|
||||
end,
|
||||
load = function(branch, save)
|
||||
branch.state:load(save)
|
||||
end,
|
||||
|
||||
active = function(branch)
|
||||
return branch.state:active()
|
||||
end,
|
||||
state = function(branch)
|
||||
return branch.state:state()
|
||||
end,
|
||||
run = function(branch, code, source, tags)
|
||||
branch.state:run(code, source, tags)
|
||||
end,
|
||||
run_file = function(branch, path, tags)
|
||||
branch.state:run_file(path, tags)
|
||||
end,
|
||||
step = function(branch)
|
||||
local event, data = branch.state:step()
|
||||
if event == "text" then
|
||||
return "text", data:to_simple_table()
|
||||
elseif event == "choice" then
|
||||
branch.choice = data
|
||||
return "choice", data:to_simple_table()
|
||||
elseif event == "return" then
|
||||
return "return", data:to_lua(branch.state)
|
||||
elseif event == "error" then
|
||||
return "error", data
|
||||
else
|
||||
return event, data
|
||||
end
|
||||
end,
|
||||
interrupt = function(branch, code, source, tags)
|
||||
branch.state:interrupt(code, source, tags)
|
||||
end,
|
||||
eval = function(branch, code, source, tags)
|
||||
return branch.state:eval(code, source, tags):to_lua(branch.state)
|
||||
end,
|
||||
eval_local = function(branch, code, source, tags)
|
||||
return branch.state:eval_local(code, source, tags):to_lua(branch.state)
|
||||
end,
|
||||
}
|
||||
|
||||
for method, fn in pairs(methods) do
|
||||
rpc.methods[method] = function(branch_id, ...)
|
||||
local branch = assert(branches[branch_id], ("can't find branch %s"):format(branch_id))
|
||||
return fn(branch, ...)
|
||||
end
|
||||
end
|
||||
|
||||
self.rpc = rpc
|
||||
self.branches = branches
|
||||
end,
|
||||
|
||||
--- Process received messages.
|
||||
--
|
||||
-- Must be called regularly.
|
||||
-- If `block` is true, the function is allowed to block execution until a message is received.
|
||||
process = function(self, block)
|
||||
self.rpc:process(block)
|
||||
end,
|
||||
}
|
||||
|
||||
return Server
|
||||
60
anselme/server/init.lua
Normal file
60
anselme/server/init.lua
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
--- Main functions to create clients and servers.
|
||||
|
||||
local Client = require("anselme.server.Client")
|
||||
|
||||
local server
|
||||
server = {
|
||||
--- Starts a Server in a new LÖVE thread and returns a Client connected to that server.
|
||||
--
|
||||
-- Should be called from a [LÖVE](https://www.love2d.org/) game code only.
|
||||
new_love_thread = function()
|
||||
local LoveThread = require("anselme.server.rpc.LoveThread")
|
||||
local input = love.thread.newChannel()
|
||||
local output = love.thread.newChannel()
|
||||
|
||||
local thread = love.thread.newThread[[
|
||||
local path, input_channel, output_channel = ...
|
||||
package.path = path
|
||||
|
||||
local LoveThread = require("anselme.server.rpc.LoveThread")
|
||||
local Server = require("anselme.server.Server")
|
||||
|
||||
local rpc = LoveThread:new(input_channel, output_channel)
|
||||
local server = Server:new(rpc)
|
||||
|
||||
while true do
|
||||
server:process(true)
|
||||
end
|
||||
]]
|
||||
thread:start(package.path, input, output)
|
||||
|
||||
return Client:new(LoveThread:new(output, input))
|
||||
end,
|
||||
|
||||
--- Returns a new Server that communicate with a Client using JSON-RPC 2.0.
|
||||
--
|
||||
-- This does not define _how_ the two comminicate (through sockets, http, etc.), you will need to define this using the `send` and `receive` arguments.
|
||||
--
|
||||
-- `send(message)` is a function that send a single message to the associated Client.
|
||||
--
|
||||
-- `receive(block)` is a function that receive a single message from the associated Client (or `nil` if no message available). If `block` is true, the function is allowed to block execution until a message is received.
|
||||
new_json_rpc_server = function(send, receive)
|
||||
local Server = require("anselme.server.Server")
|
||||
local JsonRpc = require("anselme.server.rpc.JsonRpc")
|
||||
return Server:new(JsonRpc:new(send, receive))
|
||||
end,
|
||||
--- Returns a new Client that communicate with a Server using JSON-RPC 2.0.
|
||||
--
|
||||
-- This does not define _how_ the two comminicate (through sockets, http, etc.), you will need to define this using the `send` and `receive` arguments.
|
||||
--
|
||||
-- `send(message)` is a function that send a single message to the associated Server.
|
||||
--
|
||||
-- `receive(block)` is a function that receive a single message from the associated Server (or `nil` if no message available). If `block` is true, the function is allowed to block execution until a message is received.
|
||||
new_json_rpc_client = function(send, receive)
|
||||
local Client = require("anselme.server.Client")
|
||||
local JsonRpc = require("anselme.server.rpc.JsonRpc")
|
||||
return Client:new(JsonRpc:new(send, receive))
|
||||
end,
|
||||
}
|
||||
|
||||
return server
|
||||
33
anselme/server/rpc/JsonRpc.lua
Normal file
33
anselme/server/rpc/JsonRpc.lua
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
--- Communicate over JSON-RPC 2.0.
|
||||
--
|
||||
-- You will need to implement your own `_send` and `_receive` methods to send the message over your wanted communication channel (socket, stdio, etc.).
|
||||
|
||||
local Rpc = require("anselme.server.rpc.abstract.Rpc")
|
||||
local json = require("anselme.lib.json")
|
||||
|
||||
local JsonRpc = Rpc {
|
||||
_send = nil,
|
||||
_receive = nil,
|
||||
|
||||
send = function(self, data)
|
||||
if data.error and data.error.id == nil then
|
||||
data.error.id = json.null
|
||||
end
|
||||
data.jsonrpc = "2.0"
|
||||
self._send(json.encode(data))
|
||||
end,
|
||||
|
||||
receive = function(self, block)
|
||||
return json.decode(self._receive(block))
|
||||
end,
|
||||
|
||||
-- `send(message)` is a function that send a single message to the other party
|
||||
-- `receive(block)` is a function that receive a single message from the other party (or nil if no message available). If `block` is true, the function is allowed to block execution until a message is received.
|
||||
init = function(self, send, receive)
|
||||
Rpc.init(self)
|
||||
self._send = send
|
||||
self._receive = receive
|
||||
end
|
||||
}
|
||||
|
||||
return JsonRpc
|
||||
30
anselme/server/rpc/LoveThread.lua
Normal file
30
anselme/server/rpc/LoveThread.lua
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
--- Communicate over LÖVE threads using Channels.
|
||||
|
||||
local Rpc = require("anselme.server.rpc.abstract.Rpc")
|
||||
|
||||
local LoveThread = Rpc {
|
||||
_output = nil,
|
||||
_input = nil,
|
||||
|
||||
send = function(self, data)
|
||||
self._output:push(data)
|
||||
end,
|
||||
|
||||
receive = function(self, block)
|
||||
if block then
|
||||
return self._input:demand()
|
||||
else
|
||||
return self._input:pop()
|
||||
end
|
||||
end,
|
||||
|
||||
-- `input` is the LÖVE thread Channel used to send data from the Anselme server to the game engine
|
||||
-- `output` is the LÖVE thread Channel used to send data from the game engine to the Anselme server
|
||||
init = function(self, input, output)
|
||||
Rpc.init(self)
|
||||
self._input = input
|
||||
self._output = output
|
||||
end
|
||||
}
|
||||
|
||||
return LoveThread
|
||||
196
anselme/server/rpc/abstract/Rpc.lua
Normal file
196
anselme/server/rpc/abstract/Rpc.lua
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
-- Note: this does not support multiple clients for a single server.
|
||||
|
||||
local class = require("anselme.lib.class")
|
||||
|
||||
local function default_callback() end
|
||||
local function default_error_callback(message, data)
|
||||
if data then
|
||||
error(("in rpc call: %s\n%s"):format(message, data))
|
||||
else
|
||||
error(("in rpc call: %s"):format(message))
|
||||
end
|
||||
end
|
||||
|
||||
local Rpc = class {
|
||||
--- The message exchanged are Lua table representing [JSON-RPC 2.0](https://www.jsonrpc.org/specification) Request and Response objects, with the following caveats:
|
||||
--
|
||||
-- * by-name parameters are not supported in requests;
|
||||
-- * result values in responses are always arrays (corresponding to the return list of a Lua function);
|
||||
-- * each side act both as both a client and a server.
|
||||
--
|
||||
-- These should not break compatility with any JSON-RPC 2.0 compliant service, just keep them in mind :)
|
||||
--
|
||||
-- Note however that the messages generated by this file require a couple change to be truly compliant:
|
||||
--
|
||||
-- * in error response caused by parsing errors, `id` will be unset instead of Null (since Lua considers nil values to be inexistent);
|
||||
-- * the `jsonrpc="2.0"` field isn't set;
|
||||
-- * and the message should be encoded/decoded to/from JSON obviously.
|
||||
--
|
||||
-- These are meant to be handled in the `:send` and `:receive` methods. See JsonRpc.lua.
|
||||
--
|
||||
-- Alternatively, look at LoveThread.lua if you don't care about full compliance.
|
||||
|
||||
--- Send a message.
|
||||
--
|
||||
-- `data` must be a table reprensenting a JSON-RPC 2.0 message (with the considerations noted above).
|
||||
--
|
||||
-- Must be redefined to handle whatever inter-process comminication you are using.
|
||||
send = function(self, data) error("not implemented") end,
|
||||
--- Receives a message.
|
||||
--
|
||||
-- Must be non-blocking by default; may be optionnaly blocking if `block` if true. May raise errors.
|
||||
--
|
||||
-- Returns a table reprensenting a JSON RPC-2.0 message (see above) or `nil` if nothing to receive.
|
||||
--
|
||||
-- Must be redefined to handle whatever inter-process comminication you are using.
|
||||
receive = function(self, block) error("not implemented") end,
|
||||
|
||||
--- Table of methods that can be called remotely:
|
||||
-- {
|
||||
-- ["method1"] = function(param1, param2, ...)
|
||||
-- return ret1, ret2, ...
|
||||
-- -- may raise an error
|
||||
-- end,
|
||||
-- ["method2"] = ...,
|
||||
-- ...
|
||||
-- }
|
||||
methods = nil,
|
||||
|
||||
--- Returns a new Rpc object.
|
||||
init = function(self)
|
||||
self.methods = {}
|
||||
self._callbacks = {}
|
||||
self._error_callbacks = {}
|
||||
end,
|
||||
|
||||
_callbacks = nil, -- { [call id] = callback, ... }
|
||||
_error_callbacks = nil, -- { [call id] = error_callback, ... }
|
||||
id = 0, -- last used call id
|
||||
|
||||
--- Call a remote method.
|
||||
--
|
||||
-- Parameters:
|
||||
-- * `method` (string) is the method name.
|
||||
-- * `params` (table) is the parameter list.
|
||||
-- * `callback` (function, optional) is either:
|
||||
-- * a function that will be called when the method returns. It receives all the returned values as arguments (ret1, ret2, ...). If not set, a default callback that discard all returns values will be used
|
||||
-- * the string "block", in which case the function will block until the remote method returns.
|
||||
-- * `error_callback` (function, optional) is a function that will be called if the method raise an error. It receives the error message and error details (usually the traceback; may be nil) as arguments (message, traceback). If not set, a default callback that raise an error will be used.
|
||||
--
|
||||
-- Returns:
|
||||
-- * the call id (number) if `callback` is not `"block"`
|
||||
-- * the values returned by the remote method if `callback` is `"block"`
|
||||
call = function(self, method, params, callback, error_callback)
|
||||
self.id = self.id + 1
|
||||
self:send{ method = method, params = params, id = self.id }
|
||||
if callback == "block" then
|
||||
local ok, response
|
||||
self._callbacks[self.id] = function(...) ok, response = true, { ... } end
|
||||
self._error_callbacks[self.id] = error_callback and function(...) ok, response = true, {}; error_callback(...) end or default_error_callback
|
||||
while not ok do self:process() end
|
||||
return unpack(response)
|
||||
else
|
||||
self._callbacks[self.id] = callback or default_callback
|
||||
self._error_callbacks[self.id] = error_callback or default_error_callback
|
||||
return self.id
|
||||
end
|
||||
end,
|
||||
--- Same as `:call`, but always discards all returned values and errors.
|
||||
-- NOTE unused for now
|
||||
notify = function(self, method, params)
|
||||
self:send{ method = method, params = params }
|
||||
end,
|
||||
--- Cancel callbacks associated with the call `call_id` (number).
|
||||
-- This does not stop the remote method execution.
|
||||
cancel = function(self, call_id)
|
||||
self._callbacks[call_id] = default_callback
|
||||
self._error_callbacks[call_id] = default_error_callback
|
||||
end,
|
||||
|
||||
--- Process incoming message.
|
||||
-- This should be called regularly.
|
||||
-- If `block` is true, block execution until a message is received.
|
||||
process = function(self, block)
|
||||
local s, d = pcall(self.receive, self, block)
|
||||
if not s then
|
||||
self:send{ error = { code = -32700, message = "Parse error", data = d, id = nil } }
|
||||
elseif d then
|
||||
if type(d) ~= "table" then
|
||||
self:send{ error = { code = -32600, message = "Invalid Request", id = nil } }
|
||||
else
|
||||
-- request
|
||||
if d.method then
|
||||
self:send(self:_process_request(d))
|
||||
-- response
|
||||
elseif d.result or d.error then
|
||||
self:_process_response(d)
|
||||
-- batch
|
||||
elseif #d > 0 then
|
||||
local first = d[1]
|
||||
-- request batch
|
||||
if d.method then
|
||||
local responses = {}
|
||||
for _, req in ipairs(d) do
|
||||
table.insert(responses, self:_process_request(req))
|
||||
end
|
||||
if #responses > 1 then
|
||||
self:send(responses)
|
||||
end
|
||||
-- response batch
|
||||
else
|
||||
for _, res in ipairs(d) do
|
||||
self:_process_response(res)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
--- Process a request or notification.
|
||||
-- Returns a response for requests.
|
||||
-- Returns nothing for notifications.
|
||||
_process_request = function(self, d)
|
||||
local method, params, id = d.method, d.params or {}, d.id
|
||||
if type(method) ~= "string" or type(d.params) ~= "table" then
|
||||
self:send{ error = { code = -32600, message = "Invalid Request" }, id = id }
|
||||
return
|
||||
end
|
||||
local fn = self.methods[method]
|
||||
if not fn then
|
||||
if id then self:send{ error = { code = -32601, message = ("Method not found %s"):format(method) }, id = id } end
|
||||
return
|
||||
end
|
||||
if params[1] == nil and not next(params) then
|
||||
if id then self:send{ error = { code = -32602, message = "Named parameters are not supported" }, id = id } end
|
||||
return
|
||||
end
|
||||
local r = { xpcall(fn, function(err) return { err, debug.traceback("Traceback from RPC:", 2) } end, unpack(params)) }
|
||||
if id then
|
||||
if r[1] then
|
||||
return { result = { unpack(r, 2) }, id = id }
|
||||
else
|
||||
return { error = { code = 0, message = r[2][1], data = r[2][2] }, id = id }
|
||||
end
|
||||
end
|
||||
end,
|
||||
--- Process a response.
|
||||
_process_response = function(self, d)
|
||||
local result, err, id = d.result, d.error, d.id
|
||||
if id then
|
||||
assert(self._callbacks[id], "invalid response call id")
|
||||
if err then
|
||||
assert(type(err) == "table", "error must be a table")
|
||||
self._error_callbacks[id](tostring(err.message), err.data)
|
||||
else
|
||||
assert(type(result) == "table", "result must be a table")
|
||||
self._callbacks[id](unpack(result))
|
||||
end
|
||||
self._callbacks[id] = nil
|
||||
self._error_callbacks[id] = nil
|
||||
else
|
||||
default_error_callback(tostring(err.message), err.data)
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
return Rpc
|
||||
|
|
@ -7,11 +7,11 @@ local tag_manager = require("anselme.state.tag_manager")
|
|||
local event_manager = require("anselme.state.event_manager")
|
||||
local translation_manager = require("anselme.state.translation_manager")
|
||||
local persistent_manager = require("anselme.state.persistent_manager")
|
||||
local uuid = require("anselme.common").uuid
|
||||
local parser = require("anselme.parser")
|
||||
local binser = require("anselme.lib.binser")
|
||||
local assert0 = require("anselme.common").assert0
|
||||
local operator_priority = require("anselme.common").operator_priority
|
||||
local common = require("anselme.common")
|
||||
local uuid, assert0, operator_priority = common.uuid, common.assert0, common.operator_priority
|
||||
local to_anselme = require("anselme.common.to_anselme")
|
||||
local anselme
|
||||
local Identifier, Return, Node
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ State = class {
|
|||
|
||||
--- Name of the branch associated to this State.
|
||||
branch_id = "main",
|
||||
--- State this State was branched from.
|
||||
--- State this State was branched from. `nil` if this is the main branch.
|
||||
source_branch = nil,
|
||||
|
||||
--- Return a new branch of this State.
|
||||
|
|
@ -118,7 +118,7 @@ State = class {
|
|||
self.scope:define_lua(name, value, func, raw_mode)
|
||||
end,
|
||||
--- Returns true if `name` (string) is defined in the global scope.
|
||||
--- Returns false otherwise.
|
||||
-- Returns false otherwise.
|
||||
defined = function(self, name)
|
||||
self.scope:push_global()
|
||||
local r = self:defined_local(name)
|
||||
|
|
@ -159,7 +159,7 @@ State = class {
|
|||
-- Currently active script
|
||||
_coroutine = nil,
|
||||
|
||||
--- Indicate if a script is currently loaded in this branch.
|
||||
--- Returns true if a script is currently loaded in this branch, false otherwise.
|
||||
active = function(self)
|
||||
return not not self._coroutine
|
||||
end,
|
||||
|
|
@ -178,12 +178,13 @@ State = class {
|
|||
--- Load a script in this branch. It will become the active script.
|
||||
--
|
||||
-- `code` is the code string or AST to run. If `code` is a string, `source` is the source name string to show in errors (optional).
|
||||
-- `tags` is an optional Lua table; its content will be added to the tags for the duration of the script.
|
||||
--
|
||||
-- Note that this will only load the script; execution will only start by using the `:step` method. Will error if a script is already active in this State.
|
||||
run = function(self, code, source)
|
||||
run = function(self, code, source, tags)
|
||||
assert(not self:active(), "a script is already active")
|
||||
self._coroutine = coroutine.create(function()
|
||||
local r = assert0(self:eval_local(code, source))
|
||||
local r = assert0(self:eval_local(code, source, tags))
|
||||
event_manager:complete_flush(self)
|
||||
if Return:is(r) then r = r.expression end
|
||||
return "return", r
|
||||
|
|
@ -191,17 +192,19 @@ State = class {
|
|||
end,
|
||||
--- Same as `:run`, but read the code from a file.
|
||||
-- `source` will be set as the file path.
|
||||
run_file = function(self, path)
|
||||
run_file = function(self, path, tags)
|
||||
local f = assert(io.open(path, "r"))
|
||||
local block = parser(f:read("a"), path)
|
||||
f:close()
|
||||
return self:run(block)
|
||||
return self:run(block, nil, tags)
|
||||
end,
|
||||
--- When a script is active, will resume running it until the next event.
|
||||
--
|
||||
-- Will error if no script is active.
|
||||
--
|
||||
-- Returns `event type string, event data`.
|
||||
--
|
||||
-- See the [events](#events) section for details on event data types for built-in events.
|
||||
step = function(self)
|
||||
assert(self:active(), "trying to step but no script is currently active")
|
||||
local success, type, data = coroutine.resume(self._coroutine)
|
||||
|
|
@ -218,16 +221,17 @@ State = class {
|
|||
--
|
||||
-- Will error if no script is active.
|
||||
--
|
||||
-- If `code` is given, the script will not be disabled but instead will be immediately replaced with this new script.
|
||||
-- `code`, `source` and `tags` are all optional and have the same behaviour as in `:run`.
|
||||
-- If they are given, the script will not be disabled but instead will be immediately replaced with this new script.
|
||||
-- The new script will then be started on the next `:step` and will preserve the current scope. This can be used to trigger an exit function or similar in the active script.
|
||||
--
|
||||
-- If this is called from within a running script, this will raise an `interrupt` event in order to stop the current script execution.
|
||||
interrupt = function(self, code, source)
|
||||
interrupt = function(self, code, source, tags)
|
||||
assert(self:active(), "trying to interrupt but no script is currently active")
|
||||
local called_from_script = self:state() == "running"
|
||||
if code then
|
||||
self._coroutine = coroutine.create(function()
|
||||
local r = assert0(self:eval_local(code, source))
|
||||
local r = assert0(self:eval_local(code, source, tags))
|
||||
event_manager:complete_flush(self)
|
||||
self.scope:reset() -- scope stack is probably messed up after the switch
|
||||
if Return:is(r) then r = r.expression end
|
||||
|
|
@ -244,19 +248,24 @@ State = class {
|
|||
--
|
||||
-- This can be called from outside a running script, but an error will be triggered the expression raise any event other than return.
|
||||
--
|
||||
-- `code` is the code string or AST to run. If `code` is a string, `source` is the source name string to show in errors (optional).
|
||||
-- `tags` is an optional Lua table; its content will be added to the tags for the duration of the expression.
|
||||
--
|
||||
-- * returns AST in case of success. Run `:to_lua(state)` on it to convert to a Lua value.
|
||||
-- * returns `nil, error message` in case of error.
|
||||
eval = function(self, code, source)
|
||||
eval = function(self, code, source, tags)
|
||||
self.scope:push_global()
|
||||
local r, e = self:eval_local(code, source)
|
||||
local r, e = self:eval_local(code, source, tags)
|
||||
self.scope:pop()
|
||||
return r, e
|
||||
end,
|
||||
--- Same as `:eval`, but evaluate the expression in the current scope.
|
||||
eval_local = function(self, code, source)
|
||||
eval_local = function(self, code, source, tags)
|
||||
if type(code) == "string" then code = parser(code, source) end
|
||||
local stack_size = self.scope:size()
|
||||
if tags then tag_manager:push(self, to_anselme(tags)) end
|
||||
local s, e = pcall(code.eval, code, self)
|
||||
if tags then tag_manager:pop(self) end
|
||||
if not s then
|
||||
self.scope:reset(stack_size)
|
||||
return nil, e
|
||||
|
|
|
|||
|
|
@ -65,13 +65,16 @@ return class {
|
|||
if last_type then
|
||||
local last_buffer = state.scope:get(event_buffer_identifier)
|
||||
local event_president = last_buffer:get(state, 1) -- elected representative of all concerned events
|
||||
-- yield event data
|
||||
-- build event data list
|
||||
local data = event_president:build_event_data(state, last_buffer)
|
||||
coroutine.yield(last_type, data)
|
||||
-- clear room for the future
|
||||
self:reset(state)
|
||||
-- post callback
|
||||
if event_president.post_flush_callback then event_president:post_flush_callback(state, last_buffer, data) end
|
||||
for _, event_data in ipairs(data) do
|
||||
-- yield event data
|
||||
coroutine.yield(last_type, event_data)
|
||||
-- post callback
|
||||
if event_president.post_flush_callback then event_president:post_flush_callback(state, last_buffer, event_data) end
|
||||
end
|
||||
end
|
||||
end,
|
||||
-- keep flushing until nothing is left (a flush may re-fill the buffer during its execution)
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ return {
|
|||
--
|
||||
-- ```
|
||||
-- :i = 1
|
||||
-- while(i <= 5)
|
||||
-- while($i <= 5)
|
||||
-- print(i)
|
||||
-- i += 1
|
||||
-- // 1, 2, 3, 4, 5
|
||||
|
|
@ -117,7 +117,7 @@ return {
|
|||
--
|
||||
-- ```
|
||||
-- :i = 1
|
||||
-- while(i <= 5)
|
||||
-- while($i <= 5)
|
||||
-- if(i == 3, break)
|
||||
-- print(i)
|
||||
-- i += 1
|
||||
|
|
@ -126,7 +126,7 @@ return {
|
|||
--
|
||||
-- ```
|
||||
-- :i = 1
|
||||
-- while(i <= 5)
|
||||
-- while($i <= 5)
|
||||
-- if(i == 3, continue)
|
||||
-- print(i)
|
||||
-- i += 1
|
||||
|
|
@ -135,7 +135,7 @@ return {
|
|||
--
|
||||
-- ```
|
||||
-- :i = 10
|
||||
-- while(i <= 5)
|
||||
-- while($i <= 5)
|
||||
-- print(i)
|
||||
-- i += 1
|
||||
-- else!
|
||||
|
|
|
|||
|
|
@ -16,7 +16,20 @@ return {
|
|||
if l:truthy() then
|
||||
return Boolean:new(env:defined(state, s:to_identifier()))
|
||||
else
|
||||
return Boolean:new(env:defined_in_current(state, s:to_identifier()))
|
||||
return Boolean:new(env:defined_in_current(state, s:to_symbol()))
|
||||
end
|
||||
end
|
||||
},
|
||||
{
|
||||
--- Returns true if the variable named `var` is defined in in the current scope, false otherwise.
|
||||
--
|
||||
-- If `search parent` is true, this will also search in parent scopes of the current scope.
|
||||
"defined", "(var::is string, search parent::is boolean=true)",
|
||||
function(state, s, l)
|
||||
if l:truthy() then
|
||||
return Boolean:new(state.scope:defined(s:to_identifier()))
|
||||
else
|
||||
return Boolean:new(state.scope:defined_in_current(s:to_symbol()))
|
||||
end
|
||||
end
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
-- TODO: doc in other language
|
||||
|
||||
return [[
|
||||
:@format = stdlib.format
|
||||
|
||||
:@bloc attaché = stdlib.attached block
|
||||
|
||||
:@afficher = stdlib.print
|
||||
|
|
@ -56,7 +58,7 @@ return [[
|
|||
:@arrondi = stdlib.round
|
||||
:@aléatoire = stdlib.rand
|
||||
|
||||
:@égal = stdlib.equal
|
||||
:@est égal = stdlib.is equal
|
||||
:@est = stdlib.is
|
||||
:@est une ancre = stdlib.is anchor
|
||||
:@est un booléen = stdlib.is boolean
|
||||
|
|
@ -83,4 +85,6 @@ return [[
|
|||
:@persister = stdlib.persist
|
||||
|
||||
:@écrire choix = stdlib.write choice
|
||||
|
||||
:@grouper texte par tag = stdlib.group text by tag
|
||||
]]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
--- # Strings
|
||||
-- @titlelevel 3
|
||||
|
||||
local utf8 = utf8 or require("lua-utf8")
|
||||
local utf8 = utf8 or (love and require("utf8") or require("lua-utf8"))
|
||||
local ast = require("anselme.ast")
|
||||
local String, Number = ast.String, ast.Number
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ local translation_manager = require("anselme.state.translation_manager")
|
|||
local tag_manager = require("anselme.state.tag_manager")
|
||||
local resume_manager = require("anselme.state.resume_manager")
|
||||
|
||||
local group_text_by_tag_identifier = Identifier:new("_group_text_by_tag")
|
||||
local group_text_by_tag_symbol = group_text_by_tag_identifier:to_symbol { exported = true }
|
||||
|
||||
return {
|
||||
-- text
|
||||
{
|
||||
|
|
@ -41,6 +44,37 @@ return {
|
|||
end
|
||||
},
|
||||
|
||||
{
|
||||
--- Cause future text events to be each split into separate text event every time the value of the tag with the key `tag_key` changes.
|
||||
--
|
||||
-- For example, with the following Anselme script:
|
||||
-- ```
|
||||
-- group text by tag("speaker")
|
||||
-- speaker: "John" #
|
||||
-- | A
|
||||
-- | B
|
||||
-- speaker: "Lana" #
|
||||
-- | C
|
||||
-- speaker: "John" #
|
||||
-- | D
|
||||
-- ```
|
||||
-- will produce three separate text events instead of one:
|
||||
-- * the first with the texts "A" and "B"; both with the tag `speaker="John"`
|
||||
-- * the second with the text "C"; with the tag `speaker="Lana"`
|
||||
-- * the last with the text "D"; wiith the tag `speaker="John"`
|
||||
--
|
||||
-- This setting affect will affect the whole state.
|
||||
"group text by tag", "(tag::is string)",
|
||||
function(state, tag)
|
||||
if not state.scope:defined(group_text_by_tag_identifier) then
|
||||
state.scope:define(group_text_by_tag_symbol, tag)
|
||||
else
|
||||
state.scope:set(group_text_by_tag_identifier, tag)
|
||||
end
|
||||
return Nil:new()
|
||||
end
|
||||
},
|
||||
|
||||
-- choice
|
||||
{
|
||||
--- Write a choice event to the event buffer using this text and `fn` as the function to call if the choice is selected.
|
||||
|
|
|
|||
253
doc/api.md
253
doc/api.md
|
|
@ -1,6 +1,6 @@
|
|||
This document describes how to use the main Anselme modules. This is generated automatically from the source files.
|
||||
|
||||
Note that this file only describes the `anselme` and `state.State` modules, which are only a selection of what I consider to be the "public API" of Anselme that I will try to keep stable.
|
||||
Note that this file only describes the `anselme` and `state.State` modules, as well as the `TextEventData` and `ChoiceEventData` classes, which are only a selection of what I consider to be the "public API" of Anselme that I will try to keep stable.
|
||||
If you need more advanced control on Anselme, feel free to look into the other source files to find more; the most useful functions should all be reasonably commented.
|
||||
|
||||
# anselme
|
||||
|
|
@ -15,31 +15,26 @@ local anselme = require("anselme")
|
|||
local state = anselme.new()
|
||||
state:load_stdlib()
|
||||
|
||||
-- read an anselme script file
|
||||
local f = assert(io.open("script.ans"))
|
||||
local script = anselme.parse(f:read("a"), "script.ans")
|
||||
f:close()
|
||||
|
||||
-- load the script in a new branch
|
||||
-- load an anselme script file in a new branch
|
||||
local run_state = state:branch()
|
||||
run_state:run(script)
|
||||
run_state:run_file("script.ans")
|
||||
|
||||
-- run the script
|
||||
while run_state:active() do
|
||||
local e, data = run_state:step()
|
||||
if e == "text" then
|
||||
local event, data = run_state:step()
|
||||
if event == "text" then
|
||||
for _, l in ipairs(data) do
|
||||
print(l:format(run_state))
|
||||
print(l)
|
||||
end
|
||||
elseif e == "choice" then
|
||||
elseif event == "choice" then
|
||||
for i, l in ipairs(data) do
|
||||
print(("%s> %s"):format(i, l:format(run_state)))
|
||||
print(("%s> %s"):format(i, l))
|
||||
end
|
||||
local choice = tonumber(io.read("l"))
|
||||
data:choose(choice)
|
||||
elseif e == "return" then
|
||||
elseif event == "return" then
|
||||
run_state:merge()
|
||||
elseif e == "error" then
|
||||
elseif event == "error" then
|
||||
error(data)
|
||||
end
|
||||
end
|
||||
|
|
@ -56,31 +51,31 @@ Anselme expects that `require("anselme.module")` will try loading both `anselme/
|
|||
|
||||
Global version string. Follow semver.
|
||||
|
||||
_defined at line 58 of [anselme/init.lua](../anselme/init.lua):_ `version = "2.0.0-rc1",`
|
||||
_defined at line 53 of [anselme/init.lua](../anselme/init.lua):_ `version = "2.0.0-rc1",`
|
||||
|
||||
### .versions
|
||||
|
||||
Table containing per-category version numbers. Incremented by one for any change that may break compatibility.
|
||||
|
||||
_defined at line 61 of [anselme/init.lua](../anselme/init.lua):_ `versions = {`
|
||||
_defined at line 56 of [anselme/init.lua](../anselme/init.lua):_ `versions = {`
|
||||
|
||||
#### .language
|
||||
|
||||
Version number for language and standard library changes.
|
||||
|
||||
_defined at line 63 of [anselme/init.lua](../anselme/init.lua):_ `language = 31,`
|
||||
_defined at line 58 of [anselme/init.lua](../anselme/init.lua):_ `language = 31,`
|
||||
|
||||
#### .save
|
||||
|
||||
Version number for save/AST format changes.
|
||||
|
||||
_defined at line 65 of [anselme/init.lua](../anselme/init.lua):_ `save = 7,`
|
||||
_defined at line 60 of [anselme/init.lua](../anselme/init.lua):_ `save = 7,`
|
||||
|
||||
#### .api
|
||||
|
||||
Version number for Lua API changes.
|
||||
|
||||
_defined at line 67 of [anselme/init.lua](../anselme/init.lua):_ `api = 10`
|
||||
_defined at line 62 of [anselme/init.lua](../anselme/init.lua):_ `api = 10`
|
||||
|
||||
### .parse (code, source)
|
||||
|
||||
|
|
@ -94,14 +89,14 @@ local ast = anselme.parse("1 + 2", "test")
|
|||
ast:eval(state)
|
||||
```
|
||||
|
||||
_defined at line 79 of [anselme/init.lua](../anselme/init.lua):_ `parse = function(code, source)`
|
||||
_defined at line 74 of [anselme/init.lua](../anselme/init.lua):_ `parse = function(code, source)`
|
||||
|
||||
### .parse_file (path)
|
||||
|
||||
Same as `:parse`, but reads the code from a file.
|
||||
`source` will be set as the file path.
|
||||
|
||||
_defined at line 84 of [anselme/init.lua](../anselme/init.lua):_ `parse_file = function(path)`
|
||||
_defined at line 79 of [anselme/init.lua](../anselme/init.lua):_ `parse_file = function(path)`
|
||||
|
||||
### .generate_translation_template (code, source)
|
||||
|
||||
|
|
@ -109,20 +104,20 @@ Generates and return Anselme code (as a string) that can be used as a base for a
|
|||
This will include every translatable element found in this code.
|
||||
`source` is an optional string; it will be used as the code source name in translation contexts.
|
||||
|
||||
_defined at line 93 of [anselme/init.lua](../anselme/init.lua):_ `generate_translation_template = function(code, source)`
|
||||
_defined at line 88 of [anselme/init.lua](../anselme/init.lua):_ `generate_translation_template = function(code, source)`
|
||||
|
||||
### .generate_translation_template_file (path)
|
||||
|
||||
Same as `:generate_translation_template`, but reads the code from a file.
|
||||
`source` will be set as the file path.
|
||||
|
||||
_defined at line 98 of [anselme/init.lua](../anselme/init.lua):_ `generate_translation_template_file = function(path)`
|
||||
_defined at line 93 of [anselme/init.lua](../anselme/init.lua):_ `generate_translation_template_file = function(path)`
|
||||
|
||||
### .new ()
|
||||
|
||||
Return a new [State](#state).
|
||||
|
||||
_defined at line 102 of [anselme/init.lua](../anselme/init.lua):_ `new = function()`
|
||||
_defined at line 97 of [anselme/init.lua](../anselme/init.lua):_ `new = function()`
|
||||
|
||||
|
||||
# State
|
||||
|
|
@ -151,7 +146,7 @@ _defined at line 76 of [anselme/state/State.lua](../anselme/state/State.lua):_ `
|
|||
|
||||
### .source_branch
|
||||
|
||||
State this State was branched from.
|
||||
State this State was branched from. `nil` if this is the main branch.
|
||||
|
||||
_defined at line 78 of [anselme/state/State.lua](../anselme/state/State.lua):_ `source_branch = nil,`
|
||||
|
||||
|
|
@ -199,6 +194,7 @@ _defined at line 117 of [anselme/state/State.lua](../anselme/state/State.lua):_
|
|||
|
||||
### :defined (name)
|
||||
|
||||
Returns true if `name` (string) is defined in the global scope.
|
||||
Returns false otherwise.
|
||||
|
||||
_defined at line 122 of [anselme/state/State.lua](../anselme/state/State.lua):_ `defined = function(self, name)`
|
||||
|
|
@ -234,7 +230,7 @@ _defined at line 148 of [anselme/state/State.lua](../anselme/state/State.lua):_
|
|||
|
||||
### :active ()
|
||||
|
||||
Indicate if a script is currently loaded in this branch.
|
||||
Returns true if a script is currently loaded in this branch, false otherwise.
|
||||
|
||||
_defined at line 163 of [anselme/state/State.lua](../anselme/state/State.lua):_ `active = function(self)`
|
||||
|
||||
|
|
@ -248,22 +244,23 @@ Returns `"inactive"` if no script is loaded.
|
|||
|
||||
_defined at line 171 of [anselme/state/State.lua](../anselme/state/State.lua):_ `state = function(self)`
|
||||
|
||||
### :run (code, source)
|
||||
### :run (code, source, tags)
|
||||
|
||||
Load a script in this branch. It will become the active script.
|
||||
|
||||
`code` is the code string or AST to run. If `code` is a string, `source` is the source name string to show in errors (optional).
|
||||
`tags` is an optional Lua table; its content will be added to the tags for the duration of the script.
|
||||
|
||||
Note that this will only load the script; execution will only start by using the `:step` method. Will error if a script is already active in this State.
|
||||
|
||||
_defined at line 183 of [anselme/state/State.lua](../anselme/state/State.lua):_ `run = function(self, code, source)`
|
||||
_defined at line 184 of [anselme/state/State.lua](../anselme/state/State.lua):_ `run = function(self, code, source, tags)`
|
||||
|
||||
### :run_file (path)
|
||||
### :run_file (path, tags)
|
||||
|
||||
Same as `:run`, but read the code from a file.
|
||||
`source` will be set as the file path.
|
||||
|
||||
_defined at line 194 of [anselme/state/State.lua](../anselme/state/State.lua):_ `run_file = function(self, path)`
|
||||
_defined at line 195 of [anselme/state/State.lua](../anselme/state/State.lua):_ `run_file = function(self, path, tags)`
|
||||
|
||||
### :step ()
|
||||
|
||||
|
|
@ -273,42 +270,218 @@ Will error if no script is active.
|
|||
|
||||
Returns `event type string, event data`.
|
||||
|
||||
_defined at line 205 of [anselme/state/State.lua](../anselme/state/State.lua):_ `step = function(self)`
|
||||
See the [events](#events) section for details on event data types for built-in events.
|
||||
|
||||
### :interrupt (code, source)
|
||||
_defined at line 208 of [anselme/state/State.lua](../anselme/state/State.lua):_ `step = function(self)`
|
||||
|
||||
### :interrupt (code, source, tags)
|
||||
|
||||
Stops the currently active script.
|
||||
|
||||
Will error if no script is active.
|
||||
|
||||
If `code` is given, the script will not be disabled but instead will be immediately replaced with this new script.
|
||||
`code`, `source` and `tags` are all optional and have the same behaviour as in `:run`.
|
||||
If they are given, the script will not be disabled but instead will be immediately replaced with this new script.
|
||||
The new script will then be started on the next `:step` and will preserve the current scope. This can be used to trigger an exit function or similar in the active script.
|
||||
|
||||
If this is called from within a running script, this will raise an `interrupt` event in order to stop the current script execution.
|
||||
|
||||
_defined at line 225 of [anselme/state/State.lua](../anselme/state/State.lua):_ `interrupt = function(self, code, source)`
|
||||
_defined at line 229 of [anselme/state/State.lua](../anselme/state/State.lua):_ `interrupt = function(self, code, source, tags)`
|
||||
|
||||
### :eval (code, source)
|
||||
### :eval (code, source, tags)
|
||||
|
||||
Evaluate an expression in the global scope.
|
||||
|
||||
This can be called from outside a running script, but an error will be triggered the expression raise any event other than return.
|
||||
|
||||
`code` is the code string or AST to run. If `code` is a string, `source` is the source name string to show in errors (optional).
|
||||
`tags` is an optional Lua table; its content will be added to the tags for the duration of the expression.
|
||||
|
||||
* returns AST in case of success. Run `:to_lua(state)` on it to convert to a Lua value.
|
||||
* returns `nil, error message` in case of error.
|
||||
|
||||
_defined at line 249 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval = function(self, code, source)`
|
||||
_defined at line 256 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval = function(self, code, source, tags)`
|
||||
|
||||
### :eval_local (code, source)
|
||||
### :eval_local (code, source, tags)
|
||||
|
||||
Same as `:eval`, but evaluate the expression in the current scope.
|
||||
|
||||
_defined at line 256 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval_local = function(self, code, source)`
|
||||
_defined at line 263 of [anselme/state/State.lua](../anselme/state/State.lua):_ `eval_local = function(self, code, source, tags)`
|
||||
|
||||
If you want to perform more advanced manipulation of the resulting AST nodes, look at the `ast` modules.
|
||||
In particular, every Node inherits the methods from [ast.abstract.Node](../ast/abstract/Node.lua).
|
||||
Otherwise, each Node has its own module file defined in the [ast/](../ast) directory.
|
||||
|
||||
|
||||
# Events
|
||||
|
||||
Anselme scripts communicate with the game by sending events. See the [language documentation](language.md#events) for more details on events.
|
||||
|
||||
Custom events can be defined; to do so, simply yield the coroutine with your custom event type (using `coroutine.yield("event type", event_data)`) from a function called in the anselme script.
|
||||
|
||||
For example, to add a `wait` event that pauses the script for some time, you could do something along these lines:
|
||||
```lua
|
||||
state:define("wait", "(duration::is number)", function(duration) coroutine.yield("wait", duration) end)
|
||||
waiting = false
|
||||
|
||||
-- and edit your Anselme event handler with something like:
|
||||
if not waiting then
|
||||
local event_type, event_data = run_state = run_state:step()
|
||||
if e == "wait" then
|
||||
waiting = true
|
||||
call_after_duration(event_data, function() waiting = false end)
|
||||
else
|
||||
-- handle other event types...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
And then from your Anselme script:
|
||||
```
|
||||
| Hello...
|
||||
---
|
||||
_file generated at 2024-06-04T14:22:53Z_
|
||||
wait(5)
|
||||
| ...world !
|
||||
```
|
||||
|
||||
## TextEventData
|
||||
|
||||
TextEventData represent the data returned by an event with the type `"text"`.
|
||||
See the [language documentation](language.md#texts) for more details on how to create a text event.
|
||||
|
||||
A TextEventData contains a list of [LuaText](#luatext), each LuaText representing a separate line of the text event.
|
||||
|
||||
For example, the following Anselme script:
|
||||
|
||||
```
|
||||
| Hi!
|
||||
| My name's John.
|
||||
```
|
||||
will return a text event containing two LuaTexts, the first containing the text "Hi!" and the second "My name's John.".
|
||||
|
||||
Usage:
|
||||
```lua
|
||||
local event_type, event_data = run_state:step()
|
||||
if event_type == "text" then
|
||||
-- event_data is a TextEventData, i.e. a list of LuaText
|
||||
for _, luatext in ipairs(event_data) do
|
||||
-- luatext is a list of text parts { text = "text string", tags = { ... } }
|
||||
for _, textpart in ipairs(luatext) do
|
||||
write_text_part_with_color(textpart.text, textpart.tags.color)
|
||||
end
|
||||
write_text("\n") -- for example, if we want a newline between each text line
|
||||
end
|
||||
else
|
||||
-- handle other event types...
|
||||
end
|
||||
```
|
||||
|
||||
_defined at line 87 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `local TextEventData`
|
||||
|
||||
### :group_by (tag_key)
|
||||
|
||||
Returns a list of TextEventData where the first part of each LuaText of each TextEventData has the same value for the tag `tag_key`.
|
||||
|
||||
In other words, this groups all the LuaTexts contained in this TextEventData using the `tag_key` tag and returns a list containing these groups.
|
||||
|
||||
For example, with the following Anselme script:
|
||||
```
|
||||
speaker: "John" #
|
||||
| A
|
||||
| B
|
||||
speaker: "Lana" #
|
||||
| C
|
||||
speaker: "John" #
|
||||
| D
|
||||
```
|
||||
calling `text_event_data:group_by("speaker")` will return a list of three TextEventData:
|
||||
* the first with the texts "A" and "B"; both with the tag `speaker="John"`
|
||||
* the second with the text "C"; with the tag `speaker="Lana"`
|
||||
* the last with the text "D"; wiith the tag `speaker="John"`
|
||||
|
||||
_defined at line 109 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `group_by = function(self, tag_key)`
|
||||
|
||||
|
||||
## ChoiceEventData
|
||||
|
||||
ChoiceEventData represent the data returned by an event with the type `"choice"`.
|
||||
See the [language documentation](language.md#choices) for more details on how to create a choice event.
|
||||
|
||||
A ChoiceEventData contains a list of [LuaText](#luatext), each LuaText representing a separate choice of the choice event.
|
||||
|
||||
For example, the following Anselme script:
|
||||
|
||||
```
|
||||
*| Yes!
|
||||
*| No.
|
||||
```
|
||||
will return a choice event containing two LuaTexts, the first containing the text "Yes!" and the second "No.".
|
||||
|
||||
Usage:
|
||||
```lua
|
||||
current_choice = nil
|
||||
waiting_for_choice = false
|
||||
|
||||
-- in your anselem event handling loop:
|
||||
if not waiting_for_choice then
|
||||
local event_type, event_data = run_state:step()
|
||||
if event_type == "choice" then
|
||||
-- event_data is a ChoiceEventData, i.e. a list of LuaText
|
||||
for i, luatext in ipairs(event_data) do
|
||||
write(("Choice number %s:"):format(i))
|
||||
-- luatext is a list of text parts { text = "text string", tags = { ... } }
|
||||
for _, textpart in ipairs(luatext) do
|
||||
write_choice_part_with_color(textpart.text, textpart.tags.color)
|
||||
end
|
||||
else
|
||||
-- handle other event types...
|
||||
end
|
||||
current_choice = event_data
|
||||
waiting_for_choice = true
|
||||
end
|
||||
end
|
||||
|
||||
-- somewhere in your code where choices are selected
|
||||
current_choice:select(choice_number)
|
||||
waiting_for_choice = false
|
||||
```
|
||||
|
||||
_defined at line 50 of [anselme/ast/Choice.lua](../anselme/ast/Choice.lua):_ `local ChoiceEventData = class {`
|
||||
|
||||
### :choose (choice)
|
||||
|
||||
Choose the choice at position `choice` (number).
|
||||
|
||||
A choice must be selected after receiving a choice event and before calling `:step` again.
|
||||
|
||||
_defined at line 58 of [anselme/ast/Choice.lua](../anselme/ast/Choice.lua):_ `choose = function(self, choice)`
|
||||
|
||||
|
||||
## LuaText
|
||||
|
||||
A Lua-friendly representation of an Anselme Text value.
|
||||
They appear in both TextEventData and ChoiceEventData to represent the text that has to be shown.
|
||||
|
||||
It contains a list of _text parts_, which are parts of a single text, each part potentially having differrent tags attached.
|
||||
A text will typically only consist of a single part unless it was built using text interpolation.
|
||||
|
||||
Each text part is a table containing `text` (string) and `tags` (table) properties, for example: `{ text = "text part string", tags = { color = "red" } }`.
|
||||
|
||||
_defined at line 19 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `local LuaText`
|
||||
|
||||
### .raw
|
||||
|
||||
Anselme Text value this was created from. For advanced usage only. See the source file [Text.lua](anselme/ast/Text.lua) for more information.
|
||||
|
||||
_defined at line 27 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `raw = nil,`
|
||||
|
||||
### :__tostring ()
|
||||
|
||||
Returns a text representation of the LuaText, using Anselme's default formatting. Useful for debugging.
|
||||
|
||||
Usage: `print(luatext)`
|
||||
|
||||
_defined at line 41 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `__tostring = function(self)`
|
||||
|
||||
---
|
||||
_file generated at 2024-11-17T15:00:50Z_
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
This document describes how to use the main Anselme modules. This is generated automatically from the source files.
|
||||
|
||||
Note that this file only describes the `anselme` and `state.State` modules, which are only a selection of what I consider to be the "public API" of Anselme that I will try to keep stable.
|
||||
Note that this file only describes the `anselme` and `state.State` modules, as well as the `TextEventData` and `ChoiceEventData` classes, which are only a selection of what I consider to be the "public API" of Anselme that I will try to keep stable.
|
||||
If you need more advanced control on Anselme, feel free to look into the other source files to find more; the most useful functions should all be reasonably commented.
|
||||
|
||||
# anselme
|
||||
|
|
@ -10,3 +10,40 @@ If you need more advanced control on Anselme, feel free to look into the other s
|
|||
# State
|
||||
|
||||
{{anselme/state/State.lua}}
|
||||
|
||||
# Events
|
||||
|
||||
Anselme scripts communicate with the game by sending events. See the [language documentation](language.md#events) for more details on events.
|
||||
|
||||
Custom events can be defined; to do so, simply yield the coroutine with your custom event type (using `coroutine.yield("event type", event_data)`) from a function called in the anselme script.
|
||||
|
||||
For example, to add a `wait` event that pauses the script for some time, you could do something along these lines:
|
||||
```lua
|
||||
state:define("wait", "(duration::is number)", function(duration) coroutine.yield("wait", duration) end)
|
||||
waiting = false
|
||||
|
||||
-- and edit your Anselme event handler with something like:
|
||||
if not waiting then
|
||||
local event_type, event_data = run_state = run_state:step()
|
||||
if e == "wait" then
|
||||
waiting = true
|
||||
call_after_duration(event_data, function() waiting = false end)
|
||||
else
|
||||
-- handle other event types...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
And then from your Anselme script:
|
||||
```
|
||||
| Hello...
|
||||
---
|
||||
wait(5)
|
||||
| ...world !
|
||||
```
|
||||
|
||||
{{anselme/ast/Text.lua}}
|
||||
|
||||
{{anselme/ast/Choice.lua}}
|
||||
|
||||
{{:lua text}}
|
||||
|
|
@ -2,10 +2,11 @@
|
|||
-- Behold! A documentation generator that doesn't try to be smart!
|
||||
-- Call this from the root anselme repository directory: `lua doc/gendocs.lua`
|
||||
|
||||
local utf8 = utf8 or require("lua-utf8")
|
||||
local utf8 = utf8 or (love and require("utf8") or require("lua-utf8"))
|
||||
|
||||
local files = {
|
||||
"doc/api.md",
|
||||
"doc/server.md",
|
||||
"doc/standard_library.md"
|
||||
}
|
||||
local source_link_prefix = "../"
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ comment
|
|||
|
||||
Variables can be defined and assigned using the `_=_` operator.
|
||||
|
||||
To define a variable, the left expression must be a [symbol](#symbol), for an assignment, an identifier. The left expression can also be a tuple for multiple assignments.
|
||||
To define a variable, the left expression must be a [symbol](#symbols), for an assignment, an identifier. The left expression can also be a tuple for multiple assignments.
|
||||
|
||||
```
|
||||
:x = 3 // definition
|
||||
|
|
@ -355,11 +355,11 @@ constant symbol = 13 // value checking error!
|
|||
|
||||
#### Exported variables
|
||||
|
||||
If the symbol has an export flag, the variable will be defined in the [export scope](#export-scope) instead of the current scope, i.e. will be defined for the whole file and be made accessible from outside files. See [export scope](#export-scope) for details.
|
||||
If the symbol has an `@` export flag, the variable will be defined in the [export scope](#export-scope) instead of the current scope, i.e. will be defined for the whole file and be made accessible from outside files. See [export scope](#export-scope) for details.
|
||||
|
||||
#### Alias variables
|
||||
|
||||
If the symbol has an alias flag, the variable will be an alias. Instead of directly accessing the value of the variable, the variable will:
|
||||
If the symbol has an `&` alias flag, the variable will be an alias. Instead of directly accessing the value of the variable, the variable will:
|
||||
|
||||
* when get, call its value with no argument and returns the result;
|
||||
* whet set, call its value with an assignment argument and returns the result.
|
||||
|
|
@ -752,7 +752,7 @@ $1, $2 // same as ($1), ($2) as _,_ has a precedence of 2
|
|||
|
||||
A variable can be defined and assigned a new function quickly using the function definition syntax.
|
||||
|
||||
The function definition syntax consist of a modified [symbol literal](#symbol) with a `$` right before the symbol name, followed by either a parameter list and expression or the function expression directly, in the same way as the [`$_` operator](#functions) described above.
|
||||
The function definition syntax consist of a modified [symbol literal](#symbols) with a `$` right before the symbol name, followed by either a parameter list and expression or the function expression directly, in the same way as the [`$_` operator](#functions) described above.
|
||||
|
||||
When evaluated, the function definition will create a new function and define a new variable set to this function.
|
||||
|
||||
|
|
@ -887,6 +887,8 @@ A text event value can be created using the [text literal](#text). A text is wri
|
|||
text!
|
||||
```
|
||||
|
||||
How the data returned by a Text event is structured is detailled in the [API documentation](api.md#texteventdata).
|
||||
|
||||
### Choices
|
||||
|
||||
Choice events are intended to represent a player choice in the host game. Each choice event in the buffer list is intended to represent a distinct choice.
|
||||
|
|
@ -902,6 +904,8 @@ A choice event can be written to the buffer using the `*_` operator on a text ev
|
|||
| Choice B has been selected.
|
||||
```
|
||||
|
||||
How the data returned by a Text event is structured is detailled in the [API documentation](api.md#choiceeventdata).
|
||||
|
||||
### Tags
|
||||
|
||||
Text and choice events can also carry metadata through tags. Tags are stored as a [struct](#struct).
|
||||
|
|
|
|||
336
doc/server.md
Normal file
336
doc/server.md
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
Instead of the scripts running in the same Lua process as the one of your game, Anselme can run in a Client-Server mode. This allows:
|
||||
|
||||
* Anselme to run in a separate thread and therefore not affect your game's frame times (Anselme is not very fast)
|
||||
* to use Anselme other game engine that don't use Lua
|
||||
|
||||
The _server_ is the process that holds and process the Anselme state. Typically, the server would run in a separate process or thread that your game.
|
||||
|
||||
The _client_ connects to a server and sends instructions to execute Anselmes scripts and receive the response. Typically, the client correspond to your game.
|
||||
|
||||
For now, the whole system assumes that there is a single client per server - so you should not share a single server among serveral client.
|
||||
|
||||
How the Client and Server communicate between each other is defined using a RPC object.
|
||||
Out-of-the-box, Anselme provides RPC objects that can communicate over [LÖVE](https://www.love2d.org/) threads, and over [JSON-RPC 2.0](https://www.jsonrpc.org/specification); these can be easily created using the functions in [anselme.server](#anselme_server).
|
||||
If you want to implement a custom RPC mechanism, you can look at the existing implementations in `anselme/server/rpc/`.
|
||||
|
||||
Example usage in a LÖVE game:
|
||||
```lua
|
||||
local server = require("anselme.server")
|
||||
|
||||
-- create a new client+server
|
||||
local client = server.new_love_thread()
|
||||
client:load_stdlib()
|
||||
|
||||
-- load an anselme script file in a new branch
|
||||
local run_state = client:branch("block")
|
||||
run_state:run_file("script.ans")
|
||||
|
||||
-- start script
|
||||
run_state:step(handle_event)
|
||||
|
||||
-- callback to handle an anselme event
|
||||
function handle_event(event, data)
|
||||
if event == "text" then
|
||||
show_dialog_box {
|
||||
lines = data,
|
||||
on_dialog_box_closed = function()
|
||||
run_state:step(handle_event) -- resume script
|
||||
end
|
||||
}
|
||||
elseif event == "choice" then
|
||||
show_choice_dialog_box {
|
||||
choices = data,
|
||||
on_dialog_box_closed = function(choice_number)
|
||||
run_state:choose(choice_number)
|
||||
run_state:step(handle_event)
|
||||
end
|
||||
}
|
||||
elseif event == "return" then
|
||||
run_state:merge()
|
||||
run_state:remove() -- remove branch from server
|
||||
elseif event == "error" then
|
||||
print("error in anselme thread!", data)
|
||||
run_state:remove()
|
||||
end
|
||||
end
|
||||
|
||||
function love.update()
|
||||
client:process() -- handle messages coming from the server
|
||||
end
|
||||
```
|
||||
|
||||
# anselme.server
|
||||
|
||||
Main functions to create clients and servers.
|
||||
|
||||
### .new_love_thread ()
|
||||
|
||||
Starts a Server in a new LÖVE thread and returns a Client connected to that server.
|
||||
|
||||
Should be called from a [LÖVE](https://www.love2d.org/) game code only.
|
||||
|
||||
_defined at line 10 of [anselme/server/init.lua](../anselme/server/init.lua):_ `new_love_thread = function()`
|
||||
|
||||
### .new_json_rpc_server (send, receive)
|
||||
|
||||
Returns a new Server that communicate with a Client using JSON-RPC 2.0.
|
||||
|
||||
This does not define _how_ the two comminicate (through sockets, http, etc.), you will need to define this using the `send` and `receive` arguments.
|
||||
|
||||
`send(message)` is a function that send a single message to the associated Client.
|
||||
|
||||
`receive(block)` is a function that receive a single message from the associated Client (or `nil` if no message available). If `block` is true, the function is allowed to block execution until a message is received.
|
||||
|
||||
_defined at line 41 of [anselme/server/init.lua](../anselme/server/init.lua):_ `new_json_rpc_server = function(send, receive)`
|
||||
|
||||
### .new_json_rpc_client (send, receive)
|
||||
|
||||
Returns a new Client that communicate with a Server using JSON-RPC 2.0.
|
||||
|
||||
This does not define _how_ the two comminicate (through sockets, http, etc.), you will need to define this using the `send` and `receive` arguments.
|
||||
|
||||
`send(message)` is a function that send a single message to the associated Server.
|
||||
|
||||
`receive(block)` is a function that receive a single message from the associated Server (or `nil` if no message available). If `block` is true, the function is allowed to block execution until a message is received.
|
||||
|
||||
_defined at line 53 of [anselme/server/init.lua](../anselme/server/init.lua):_ `new_json_rpc_client = function(send, receive)`
|
||||
|
||||
|
||||
# Client
|
||||
|
||||
This is a Lua implementation of an Anselme client, with a nice API that mirrors the Anselme [State API](api.md#state) to communicate with the server.
|
||||
|
||||
Usage: create a Client object using the functions in the [anselme.server module](#anselme_server) and call `server:process()` regularly to process messages from the Server.
|
||||
|
||||
The API available here tries to follow the [State API](api.md#state) as much as possible, with the following differences:
|
||||
* functions that return a value in State take an additionnal argument `callback`:
|
||||
* if it is a function `callback(ret1, ret2, ...)`, it is called as soon as the return values `ret1, ret2, ...` are received. The function also returns the identifier `call_id` associated with the callback (to optionally cancel the callback later using `client:cancel(call_id)`).
|
||||
* if it is `nil`, return values are discarded;
|
||||
* if it is the string `"block"`, the call will block until the return values are received. The function returns these values directly.
|
||||
* functions that returns a `State` in State now returns a `Client`;
|
||||
* return values are converted to a simpler representation if possible (no metamethods, userdata or cycles) to make serialization simpler - in particular, Anselme values are automatically converted to Lua primitives.
|
||||
* a few new methods are introduced, see below.
|
||||
|
||||
Implementing a Client in other languages should be relatively easy: if your client language has a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) library, point it to the Anselme server you started using [`server.new_json_rpc_server()`](#new_json_rpc_server) and you're done.
|
||||
You should then be able to call any of the methods described in the [Server](#server).
|
||||
Additionnaly, if you plan to use the `define_rpc` or `define_local_rpc` server methods, you will need to implement the following remote method in your client that will be called by the server:
|
||||
* `call(function_id, ...)` where `function_id` (string) is the function identifier that was given when `define_rpc` or `define_local_rpc` was called, and `...` is a list of arguments. This must call the function associated with the `function_id` using the given arguments, and returns the values returned by the call (as a list of return values: `{ret1, ret2, ...}`).
|
||||
|
||||
### :process (block)
|
||||
|
||||
Process received messages.
|
||||
|
||||
Must be called regularly.
|
||||
If `block` is true, the function is allowed to block execution until a message is received.
|
||||
|
||||
_defined at line 51 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `process = function(self, block)`
|
||||
|
||||
### :cancel (call_id)
|
||||
|
||||
Cancel the callback associated with the call `call_id`.
|
||||
This does not stop the remote method execution; only prevent the callback from being called.
|
||||
|
||||
_defined at line 57 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `cancel = function(self, call_id)`
|
||||
|
||||
### :choose (i)
|
||||
|
||||
If the last event was a `choice`, choose the `i`-th choice.
|
||||
This must be called before calling `:step` again after receiving a choice event.
|
||||
|
||||
_defined at line 63 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `choose = function(self, i)`
|
||||
|
||||
### :remove ()
|
||||
|
||||
Remove the branch from the server.
|
||||
The branch (and therefore this Client branch) can't be used after calling this method.
|
||||
|
||||
_defined at line 68 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `remove = function(self)`
|
||||
|
||||
### :define_rpc (name, args, func)
|
||||
|
||||
Defines a function in the global scope, that calls the Lua function `func` on the Client when called.
|
||||
|
||||
The function will not be sent to the server; it will be directly executed on the client (i.e. your game code)
|
||||
each time a script on the server needs it to be called.
|
||||
|
||||
Usage: `client:define_rpc("teleport", "(position)", function(position) player:teleport(position) end)`
|
||||
|
||||
_defined at line 78 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `define_rpc = function(self, name, args, func)`
|
||||
|
||||
### :define_local_rpc (name, args, func)
|
||||
|
||||
Same as `:define_rpc`, but define the function in the current scope.
|
||||
|
||||
_defined at line 85 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `define_local_rpc = function(self, name, args, func)`
|
||||
|
||||
## Methods and fields that mirror the State API
|
||||
|
||||
### :load_stdlib (language)
|
||||
|
||||
Same as [`state:load_stdlib(language)`](api.md#load_stdlib-language).
|
||||
|
||||
_defined at line 95 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `load_stdlib = function(self, language)`
|
||||
|
||||
### .branch_id
|
||||
|
||||
Same as [`state.branch_id`](api.md#branch_id).
|
||||
|
||||
_defined at line 100 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `branch_id = "main",`
|
||||
|
||||
### .source_branch
|
||||
|
||||
Same as [`state.source_branch`](api.md#source_branch), but refers to the source `Client` instead of a `State`.
|
||||
|
||||
_defined at line 102 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `source_branch = nil,`
|
||||
|
||||
### :branch (branch_id, callback)
|
||||
|
||||
Same as [`state:branch(branch_id)`](api.md#branch-branch_id), but returns a new `Client` instead of a `State`.
|
||||
|
||||
_defined at line 104 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `branch = function(self, branch_id, callback)`
|
||||
|
||||
### :merge ()
|
||||
|
||||
Same as [`state:merge()`](api.md#merge).
|
||||
|
||||
_defined at line 113 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `merge = function(self)`
|
||||
|
||||
### :define (name, value, func_code, raw_mode)
|
||||
|
||||
Same as [`state:define(name, value, func, raw_mode)`](api.md#api.md#define-name-value-func-raw_mode), but if `func_code` is given, it must be a string containing the function code.
|
||||
|
||||
Note that the given code will be executed on the server, and that there is no sandboxing of any kind;
|
||||
|
||||
Example: `client:define("main", "print", "(message::is string)", "function(message) print(message) end")`.
|
||||
|
||||
_defined at line 122 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `define = function(self, name, value, func_code, raw_mode)`
|
||||
|
||||
### :define_local (name, value, func_code, raw_mode)
|
||||
|
||||
Same as [`define`](#define-name-value-func_code-raw_mode), but calls [`state:define_local(name, value, func, raw_mode)`](api.md#api.md#define_local-name-value-func-raw_mode).
|
||||
|
||||
_defined at line 126 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `define_local = function(self, name, value, func_code, raw_mode)`
|
||||
|
||||
### :defined (name, callback)
|
||||
|
||||
Same as [`state:defined(name)`](api.md#defined-name).
|
||||
|
||||
_defined at line 130 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `defined = function(self, name, callback)`
|
||||
|
||||
### :defined_local (name, callback)
|
||||
|
||||
Same as [`state:defined_local(name)`](api.md#defined_local-name).
|
||||
|
||||
_defined at line 134 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `defined_local = function(self, name, callback)`
|
||||
|
||||
### :save (callback)
|
||||
|
||||
Same as [`state:save()`](api.md#save).
|
||||
|
||||
_defined at line 139 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `save = function(self, callback)`
|
||||
|
||||
### :load (save)
|
||||
|
||||
Same as [`state:load(save)`](api.md#load-save).
|
||||
|
||||
_defined at line 143 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `load = function(self, save)`
|
||||
|
||||
### :active (callback)
|
||||
|
||||
Same as [`state:active()`](api.md#active).
|
||||
|
||||
_defined at line 148 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `active = function(self, callback)`
|
||||
|
||||
### :state (callback)
|
||||
|
||||
Same as [`state:state()`](api.md#state).
|
||||
|
||||
_defined at line 152 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `state = function(self, callback)`
|
||||
|
||||
### :run (code, source, tags)
|
||||
|
||||
Same as [`state:run(code, source, tags)`](api.md#run-code-source-tags).
|
||||
|
||||
_defined at line 156 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `run = function(self, code, source, tags)`
|
||||
|
||||
### :run_file (path, tags)
|
||||
|
||||
Same as [`state:run_file(code, source, tags)`](api.md#run_file-code-source-tags).
|
||||
|
||||
_defined at line 160 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `run_file = function(self, path, tags)`
|
||||
|
||||
### :step (callback)
|
||||
|
||||
Same as [`state:step)`](api.md#step), but returns:
|
||||
* for `text` and `choice` events, a list of lines `{ { { text = "line 1 part 2", tags = { ... } }, ... }, ... }` (in other word, [`TextEventData`](api.md#texteventdata) and [`ChoiceEventData`](api.md#choiceeventdata) stripped of everything but their list of text parts);
|
||||
* for `return` events, the return value converted to Lua primitives;
|
||||
* for other events, it will try to return the event data as-is.
|
||||
|
||||
_defined at line 167 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `step = function(self, callback)`
|
||||
|
||||
### :interrupt (code, source, tags)
|
||||
|
||||
Same as [`state:interrupt(code, source, tags)`](api.md#interrupt-code-source-tags).
|
||||
|
||||
_defined at line 171 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `interrupt = function(self, code, source, tags)`
|
||||
|
||||
### :eval (code, source, tags, callback)
|
||||
|
||||
Same as [`state:eval(code, source, tags)`](api.md#eval-code-source-tags), but the returned value is converted to Lua primitives.
|
||||
|
||||
_defined at line 175 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `eval = function(self, code, source, tags, callback)`
|
||||
|
||||
### :eval_local (code, source, tags, callback)
|
||||
|
||||
Same as [`state:eval_local(code, source, tags)`](api.md#eval_local-code-source-tags), but the returned value is converted to Lua primitives.
|
||||
|
||||
_defined at line 179 of [anselme/server/Client.lua](../anselme/server/Client.lua):_ `eval_local = function(self, code, source, tags, callback)`
|
||||
|
||||
|
||||
# Server
|
||||
|
||||
An Anselme server instance.
|
||||
|
||||
Usage: create a Server object using the functions in the [anselme.server module](#anselme_server) and call `server:process()` regularly to process messages from the Client.
|
||||
|
||||
If you are implementing your own client, the following methods are available to be remotely called by your client:
|
||||
* Note:
|
||||
* in all the following methods, the first parameter `branch_id` (string) is the id of the Anselme branch to operate on;
|
||||
* methods that return something always returns a list of return values: `{ ret1, ret2, ... }`.
|
||||
* `choose(branch_id, i)`: if the last event was a `choice`, choose the `i`-th (number) line in the choice list;
|
||||
* `remove(branch_id)`: removes the branch from the server; no further operation will be possible on the branch;
|
||||
* `load_stdlib(branch_id, language)`: calls [`state:load_stdlib(language)`](api.md#load_stdlib-language) on the branch;
|
||||
* `branch(branch_id[, new_branch_id])`: calls [`state:branch(branch_id)`](api.md#branch-branch_id) on the branch; returns the id of the new branch (string);
|
||||
* `merge(branch_id)`: calls [`state:merge()`](api.md#merge) on the branch;
|
||||
* `define(branch_id, name, args, func_code, raw_mode)`: calls [`state:define(branch_id, name, args, func, raw_mode)`](api.md#define-name-value-func-raw_mode) on the branch; if `func_code` is given, `func` will be a function generated from the Lua code `func_code` (string, example: `define("main", "print", "(message::is string)", "function(message) print(message) end")`). Note that whatever is in `func_code` will be executed on the server, and that there is no sandboxing of any kind;
|
||||
* `define_rpc(branch_id, name, args, func_id)`: defines a function in the branch that, when called, will call the remote method `call(func_id, ...)` on the client and block until it returns. In other words, this allows the Anselme script running on the server to transparently call the function that is associated with the id `func_id` on the client.
|
||||
* `define_local(branch_id, name, args, func_code, raw_mode)`: same as `define`, but calls [`state:define_local(branch_id, name, args, func, raw_mode)`](api.md#define_local-name-value-func-raw_mode);
|
||||
* `define_local_rpc(branch_id, name, args, func_id)`: same as `define_rpc`, but defines the function in the current scope;
|
||||
* `defined(branch_id, name)`: calls [`state:defined(name)`](api.md#defined-name) on the branch and returns its result;
|
||||
* `defined_local(branch_id, name)`: calls [`state:defined_local(name)`](api.md#defined_local-name) on the branch and returns its result;
|
||||
* `save(branch_id)`: calls [`state:save()`](api.md#save) on the branch and returns its result;
|
||||
* `load(branch_id, save)`: calls [`state:load(save)`](api.md#load-save) on the branch;
|
||||
* `active(branch_id)`: calls [`state:active()`](api.md#active) on the branch and returns its result;
|
||||
* `state(branch_id)`: calls [`state:state()`](api.md#state) on the branch and returns its result;
|
||||
* `run(branch_id, code, source, tags)`: calls [`state:run(code, source, tags)`](api.md#run-code-source-tags) on the branch;
|
||||
* `run_file(branch_id, path, tags)`: calls [`state:run_file(path, tags)`](api.md#run_file-path-tags) on the branch;
|
||||
* `step(branch_id)`: calls [`state:step()`](api.md#step) on the branch and returns:
|
||||
* for `text` and `choices` events, a list of lines `{ { { text = "line 1 part 2", tags = { ... } }, ... }, ... }` (in other word, [`TextEventData`](api.md#texteventdata) and [`ChoiceEventData`](api.md#choiceeventdata) stripped of everything but their list of text parts);
|
||||
* for `return` events, the return value converted to Lua;
|
||||
* for other events, it will try to return the event data as-is.
|
||||
* `interrupt(branch_id, code, source, tags)`: calls [`state:interrupt(code, source, tags)`](api.md#interrupt-code-source-tags) on the branch;
|
||||
* `eval(branch_id, code, source, tags)`: calls [`state:eval(code, source, tags)`](api.md#eval-code-source-tags) on the branch and returns its result, converted to Lua;
|
||||
* `eval_local(branch_id, code, source, tags)`: calls [`state:eval_local(code, source, tags)`](api.md#eval_local-code-source-tags) on the branch and returns its result, converted to Lua.
|
||||
|
||||
### :process (block)
|
||||
|
||||
Process received messages.
|
||||
|
||||
Must be called regularly.
|
||||
If `block` is true, the function is allowed to block execution until a message is received.
|
||||
|
||||
_defined at line 160 of [anselme/server/Server.lua](../anselme/server/Server.lua):_ `process = function(self, block)`
|
||||
|
||||
|
||||
---
|
||||
_file generated at 2024-11-17T15:00:50Z_
|
||||
72
doc/server.md.template
Normal file
72
doc/server.md.template
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
Instead of the scripts running in the same Lua process as the one of your game, Anselme can run in a Client-Server mode. This allows:
|
||||
|
||||
* Anselme to run in a separate thread and therefore not affect your game's frame times (Anselme is not very fast)
|
||||
* to use Anselme other game engine that don't use Lua
|
||||
|
||||
The _server_ is the process that holds and process the Anselme state. Typically, the server would run in a separate process or thread that your game.
|
||||
|
||||
The _client_ connects to a server and sends instructions to execute Anselmes scripts and receive the response. Typically, the client correspond to your game.
|
||||
|
||||
For now, the whole system assumes that there is a single client per server - so you should not share a single server among serveral client.
|
||||
|
||||
How the Client and Server communicate between each other is defined using a RPC object.
|
||||
Out-of-the-box, Anselme provides RPC objects that can communicate over [LÖVE](https://www.love2d.org/) threads, and over [JSON-RPC 2.0](https://www.jsonrpc.org/specification); these can be easily created using the functions in [anselme.server](#anselme_server).
|
||||
If you want to implement a custom RPC mechanism, you can look at the existing implementations in `anselme/server/rpc/`.
|
||||
|
||||
Example usage in a LÖVE game:
|
||||
```lua
|
||||
local server = require("anselme.server")
|
||||
|
||||
-- create a new client+server
|
||||
local client = server.new_love_thread()
|
||||
client:load_stdlib()
|
||||
|
||||
-- load an anselme script file in a new branch
|
||||
local run_state = client:branch("block")
|
||||
run_state:run_file("script.ans")
|
||||
|
||||
-- start script
|
||||
run_state:step(handle_event)
|
||||
|
||||
-- callback to handle an anselme event
|
||||
function handle_event(event, data)
|
||||
if event == "text" then
|
||||
show_dialog_box {
|
||||
lines = data,
|
||||
on_dialog_box_closed = function()
|
||||
run_state:step(handle_event) -- resume script
|
||||
end
|
||||
}
|
||||
elseif event == "choice" then
|
||||
show_choice_dialog_box {
|
||||
choices = data,
|
||||
on_dialog_box_closed = function(choice_number)
|
||||
run_state:choose(choice_number)
|
||||
run_state:step(handle_event)
|
||||
end
|
||||
}
|
||||
elseif event == "return" then
|
||||
run_state:merge()
|
||||
run_state:remove() -- remove branch from server
|
||||
elseif event == "error" then
|
||||
print("error in anselme thread!", data)
|
||||
run_state:remove()
|
||||
end
|
||||
end
|
||||
|
||||
function love.update()
|
||||
client:process() -- handle messages coming from the server
|
||||
end
|
||||
```
|
||||
|
||||
# anselme.server
|
||||
|
||||
{{anselme/server/init.lua}}
|
||||
|
||||
# Client
|
||||
|
||||
{{anselme/server/Client.lua}}
|
||||
|
||||
# Server
|
||||
|
||||
{{anselme/server/Server.lua}}
|
||||
|
|
@ -296,7 +296,7 @@ If a `break` happens in the loop, the whole loop is stopped.
|
|||
|
||||
```
|
||||
:i = 1
|
||||
while(i <= 5)
|
||||
while($i <= 5)
|
||||
print(i)
|
||||
i += 1
|
||||
// 1, 2, 3, 4, 5
|
||||
|
|
@ -304,7 +304,7 @@ while(i <= 5)
|
|||
|
||||
```
|
||||
:i = 1
|
||||
while(i <= 5)
|
||||
while($i <= 5)
|
||||
if(i == 3, break)
|
||||
print(i)
|
||||
i += 1
|
||||
|
|
@ -313,7 +313,7 @@ while(i <= 5)
|
|||
|
||||
```
|
||||
:i = 1
|
||||
while(i <= 5)
|
||||
while($i <= 5)
|
||||
if(i == 3, continue)
|
||||
print(i)
|
||||
i += 1
|
||||
|
|
@ -322,7 +322,7 @@ while(i <= 5)
|
|||
|
||||
```
|
||||
:i = 10
|
||||
while(i <= 5)
|
||||
while($i <= 5)
|
||||
print(i)
|
||||
i += 1
|
||||
else!
|
||||
|
|
@ -650,19 +650,43 @@ _defined at line 21 of [anselme/stdlib/string.lua](../anselme/stdlib/string.lua)
|
|||
|
||||
Concatenate two texts, returning a new text value.
|
||||
|
||||
_defined at line 16 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"_+_", "(a::is text, b::is text)",`
|
||||
_defined at line 19 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"_+_", "(a::is text, b::is text)",`
|
||||
|
||||
### txt::is text !
|
||||
|
||||
Write a text event in the event buffer using this text.
|
||||
|
||||
_defined at line 30 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"_!", "(txt::is text)",`
|
||||
_defined at line 33 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"_!", "(txt::is text)",`
|
||||
|
||||
### tag (txt::is text, tags::is struct)
|
||||
|
||||
Create and return a new text from `text`, with the tags from `tags` added.
|
||||
|
||||
_defined at line 38 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"tag", "(txt::is text, tags::is struct)",`
|
||||
_defined at line 41 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"tag", "(txt::is text, tags::is struct)",`
|
||||
|
||||
### group text by tag (tag::is string)
|
||||
|
||||
Cause future text events to be each split into separate text event every time the value of the tag with the key `tag_key` changes.
|
||||
|
||||
For example, with the following Anselme script:
|
||||
```
|
||||
group text by tag("speaker")
|
||||
speaker: "John" #
|
||||
| A
|
||||
| B
|
||||
speaker: "Lana" #
|
||||
| C
|
||||
speaker: "John" #
|
||||
| D
|
||||
```
|
||||
will produce three separate text events instead of one:
|
||||
* the first with the texts "A" and "B"; both with the tag `speaker="John"`
|
||||
* the second with the text "C"; with the tag `speaker="Lana"`
|
||||
* the last with the text "D"; wiith the tag `speaker="John"`
|
||||
|
||||
This setting affect will affect the whole state.
|
||||
|
||||
_defined at line 67 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"group text by tag", "(tag::is string)",`
|
||||
|
||||
### write choice (text::is text, fn=attached block(keep return=true, default=($()())))
|
||||
|
||||
|
|
@ -678,13 +702,13 @@ write choice(| Choice |, $42)
|
|||
|
||||
If we are currently resuming to an anchor contained in `fn`, `fn` is directly called and the current choice event buffer will be discarded on flush, simulating the choice event buffer being sent to the host game and this choice being selected.
|
||||
|
||||
_defined at line 57 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"write choice", "(text::is text, fn=attached block(keep return=true, default=($()())))",`
|
||||
_defined at line 91 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"write choice", "(text::is text, fn=attached block(keep return=true, default=($()())))",`
|
||||
|
||||
### original -> translated
|
||||
|
||||
Add a translation so `original` is replaced with `translated`.
|
||||
|
||||
_defined at line 74 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"_->_", "(original::is(\"quote\"), translated::is(\"quote\"))",`
|
||||
_defined at line 108 of [anselme/stdlib/text.lua](../anselme/stdlib/text.lua):_ `"_->_", "(original::is(\"quote\"), translated::is(\"quote\"))",`
|
||||
|
||||
|
||||
# Symbols
|
||||
|
|
@ -1135,23 +1159,31 @@ If `search parent` is true, this will also search in parent scopes of the enviro
|
|||
|
||||
_defined at line 14 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"defined", "(env::is environment, var::is string, search parent::is boolean=false)",`
|
||||
|
||||
### defined (var::is string, search parent::is boolean=true)
|
||||
|
||||
Returns true if the variable named `var` is defined in in the current scope, false otherwise.
|
||||
|
||||
If `search parent` is true, this will also search in parent scopes of the current scope.
|
||||
|
||||
_defined at line 27 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"defined", "(var::is string, search parent::is boolean=true)",`
|
||||
|
||||
### c::is environment . s::is string
|
||||
|
||||
Gets the variable named `s` defined in the environment `c`.
|
||||
|
||||
_defined at line 26 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"_._", "(c::is environment, s::is string)",`
|
||||
_defined at line 39 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"_._", "(c::is environment, s::is string)",`
|
||||
|
||||
### c::is environment . s::is string = v
|
||||
|
||||
Sets the variable named `s` defined in the environment `c` to `v`.
|
||||
|
||||
_defined at line 35 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"_._", "(c::is environment, s::is string) = v",`
|
||||
_defined at line 48 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"_._", "(c::is environment, s::is string) = v",`
|
||||
|
||||
### c::is environment . s::is symbol = v
|
||||
|
||||
Define a new variable `s` in the environment `c` with the value `v`.
|
||||
|
||||
_defined at line 45 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"_._", "(c::is environment, s::is symbol) = v",`
|
||||
_defined at line 58 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"_._", "(c::is environment, s::is symbol) = v",`
|
||||
|
||||
### import (env::is environment, symbol tuple::is tuple)
|
||||
|
||||
|
|
@ -1164,7 +1196,7 @@ import(env, [:a, :b])
|
|||
:b = env.b
|
||||
```
|
||||
|
||||
_defined at line 63 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"import", "(env::is environment, symbol tuple::is tuple)",`
|
||||
_defined at line 76 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"import", "(env::is environment, symbol tuple::is tuple)",`
|
||||
|
||||
### import (env::is environment, symbol::is symbol)
|
||||
|
||||
|
|
@ -1176,14 +1208,14 @@ import(env, :a)
|
|||
:a = env.a
|
||||
```
|
||||
|
||||
_defined at line 79 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"import", "(env::is environment, symbol::is symbol)",`
|
||||
_defined at line 92 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"import", "(env::is environment, symbol::is symbol)",`
|
||||
|
||||
### load (path::is string)
|
||||
|
||||
Load an Anselme script from a file and run it.
|
||||
Returns the environment containing the exported variables from the file.
|
||||
|
||||
_defined at line 89 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"load", "(path::is string)",`
|
||||
_defined at line 102 of [anselme/stdlib/environment.lua](../anselme/stdlib/environment.lua):_ `"load", "(path::is string)",`
|
||||
|
||||
|
||||
# Typed values
|
||||
|
|
@ -1336,4 +1368,4 @@ _defined at line 14 of [anselme/stdlib/wrap.lua](../anselme/stdlib/wrap.lua):_ `
|
|||
|
||||
|
||||
---
|
||||
_file generated at 2024-06-04T14:22:53Z_
|
||||
_file generated at 2024-11-17T15:00:50Z_
|
||||
|
|
@ -225,7 +225,7 @@ else!
|
|||
|
||||
```
|
||||
:i = 0
|
||||
while(i < 5)
|
||||
while($i < 5)
|
||||
i += 1
|
||||
if(i == 4)
|
||||
break!
|
||||
|
|
@ -240,7 +240,7 @@ for(:x, range(10))
|
|||
|
||||
# Persistence
|
||||
|
||||
_For more detailled information on this, look at [persistence](standard_library.md#persistence-helpers), [alias variables](#api.md#alias-variables)._
|
||||
_For more detailled information on this, look at [persistence](standard_library.md#persistence-helpers), [alias variables](language.md#alias-variables)._
|
||||
|
||||
Variables that needs to be saved and loaded alongside the game's save files can be stored in persistent storage.
|
||||
|
||||
|
|
@ -333,4 +333,4 @@ Text and any value preceded by a `%` prefix will be replaced with its translatio
|
|||
%"red" // "rouge"
|
||||
```
|
||||
|
||||
An Anselme script containing a ready-to-translate list of all translatables elements of a file can be obtained using the [translation template generator Lua API methods](#api.md#generate-translation-template). The template can then be loaded as a regular Anselme file for all the translations to be applied.
|
||||
An Anselme script containing a ready-to-translate list of all translatables elements of a file can be obtained using the [translation template generator Lua API methods](api.md#generate-translation-template). The template can then be loaded as a regular Anselme file for all the translations to be applied.
|
||||
|
|
|
|||
12
ideas.md
12
ideas.md
|
|
@ -4,19 +4,13 @@ Loosely ordered by willingness to implement.
|
|||
|
||||
---
|
||||
|
||||
Translation.
|
||||
|
||||
Do some more fancy scope work to allow the translation to access variables defined in the translation file?
|
||||
Redundant `TextEventData:group_by` and `stdlib.group text by tag`: there can be only one.
|
||||
|
||||
---
|
||||
|
||||
Server API.
|
||||
Translation.
|
||||
|
||||
To be able to use Anselme in another language, it would be nice to be able to access it over some form of IPC.
|
||||
|
||||
No need to bother with networking I think. Just do some stdin/stdout handling, maybe use something like JSON-RPC: https://www.jsonrpc.org/specification (reminder: will need to add some metadata to specify content length, not aware of any streaming json lib in pure Lua - here's a rxi seal of quality library btw: https://github.com/rxi/json.lua). Or just make our own protocol around JSON.
|
||||
Issue: how to represent Anselme values? they will probably contain cycles, needs to access their methods, etc.
|
||||
Probably wise to look into how other do it. LSP: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
|
||||
Do some more fancy scope work to allow the translation to access variables defined in the translation file?
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ The overengineered game dialog scripting system in pure Lua.
|
|||
|
||||
This is version 2, a full rewrite. Version 1 is still available in the v1 branch.
|
||||
|
||||
Supported: Lua 5.4, Lua 5.3, LuaJIT (LuaJIT requires the utf8 module: `luarocks --lua-version=5.1 install luautf8`).
|
||||
Supported: Lua 5.4, Lua 5.3, LÖVE, LuaJIT (LuaJIT requires the utf8 module: `luarocks --lua-version=5.1 install luautf8`).
|
||||
Otherwise all needed files are included in the `anselme` directory.
|
||||
|
||||
Anselme is licensed under the ISC license, meaning you can basically use it for anything as long as you make the content of the [license file](license) appear somewhere. I would appreciate it if you don't use Anselme to commit war crimes though. If that's not enough for you or want better support, feel free to contact me, my integrity can be bought.
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ local function run_loop(run_state, write_output, interactive)
|
|||
for _, v in ipairs(grouped) do
|
||||
if groups then write_output(":: group ::") end
|
||||
for _, l in ipairs(v) do
|
||||
write_output(l:format(run_state))
|
||||
write_output(l.raw:format(run_state))
|
||||
end
|
||||
end
|
||||
elseif e == "choice" then
|
||||
local choice
|
||||
if interactive then
|
||||
for i, l in ipairs(data) do
|
||||
write_output(("%s> %s"):format(i, l:format(run_state)))
|
||||
write_output(("%s> %s"):format(i, l.raw:format(run_state)))
|
||||
end
|
||||
io.write(("Select choice (1-%s): "):format(#data))
|
||||
choice = tonumber(io.read("l"))
|
||||
|
|
@ -53,9 +53,9 @@ local function run_loop(run_state, write_output, interactive)
|
|||
choice = assert(run_state:eval_local("choice"), "no choice selected"):to_lua()
|
||||
for i, l in ipairs(data) do
|
||||
if i == choice then
|
||||
write_output(("=> %s"):format(l:format(run_state)))
|
||||
write_output(("=> %s"):format(l.raw:format(run_state)))
|
||||
else
|
||||
write_output((" > %s"):format(l:format(run_state)))
|
||||
write_output((" > %s"):format(l.raw:format(run_state)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue