1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-27 08:39:30 +00:00

Compare commits

..

2 commits

28 changed files with 1658 additions and 66 deletions

View file

@ -57,7 +57,18 @@ local ChoiceEventData = class {
-- A choice must be selected after receiving a choice event and before calling `:step` again. -- A choice must be selected after receiving a choice event and before calling `:step` again.
choose = function(self, choice) choose = function(self, choice)
self._selected = 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 local Choice
@ -86,7 +97,7 @@ Choice = ast.abstract.Runtime(Event) {
for _, c in event_buffer:iter(state) do for _, c in event_buffer:iter(state) do
table.insert(l, c.text:to_lua(state)) table.insert(l, c.text:to_lua(state))
end end
return l return { l }
end, end,
post_flush_callback = function(self, state, event_buffer, data) post_flush_callback = function(self, state, event_buffer, data)
local choice = data._selected local choice = data._selected

View file

@ -5,6 +5,8 @@ local ArgumentTuple, Struct
local to_anselme = require("anselme.common.to_anselme") local to_anselme = require("anselme.common.to_anselme")
local group_text_by_tag_identifier
--- A Lua-friendly representation of an Anselme Text value. --- 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. -- They appear in both TextEventData and ChoiceEventData to represent the text that has to be shown.
-- --
@ -38,7 +40,18 @@ LuaText = class {
-- @defer lua text -- @defer lua text
__tostring = function(self) __tostring = function(self)
return self.raw:format(self._state) return self.raw:format(self._state)
end 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"`. --- TextEventData represent the data returned by an event with the type `"text"`.
@ -75,9 +88,9 @@ local TextEventData
TextEventData = class { TextEventData = class {
-- [1] = LuaText, ... -- [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_name`. --- 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_name` tag and returns a list containing these groups. -- 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: -- For example, with the following Anselme script:
-- ``` -- ```
@ -93,10 +106,10 @@ TextEventData = class {
-- * the first with the texts "A" and "B"; both with the tag `speaker="John"` -- * 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 second with the text "C"; with the tag `speaker="Lana"`
-- * the last with the text "D"; wiith the tag `speaker="John"` -- * the last with the text "D"; wiith the tag `speaker="John"`
group_by = function(self, tag_name) group_by = function(self, tag_key)
if type(tag_key) == "string" then tag_key = to_anselme(tag_key) end
local l = {} local l = {}
local current_group local current_group
local tag_key = to_anselme(tag_name)
local last_value local last_value
for _, luatext in ipairs(self) do for _, luatext in ipairs(self) do
local list = luatext.raw.list local list = luatext.raw.list
@ -111,7 +124,18 @@ TextEventData = class {
end end
end end
return l return l
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 Text local Text
@ -169,11 +193,17 @@ Text = Runtime(Event) {
for _, text in event_buffer:iter(state) do for _, text in event_buffer:iter(state) do
table.insert(l, text:to_lua(state)) table.insert(l, text:to_lua(state))
end end
return l 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
end, end,
} }
package.loaded[...] = Text package.loaded[...] = Text
ArgumentTuple, Struct = ast.ArgumentTuple, ast.Struct ArgumentTuple, Struct = ast.ArgumentTuple, ast.Struct
group_text_by_tag_identifier = ast.Identifier:new("_group_text_by_tag")
return Text return Text

View file

@ -6,7 +6,7 @@ return ast.abstract.Node {
type = "event", type = "event",
init = false, 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) build_event_data = function(self, state, event_buffer)
error("build_event_data not implemented for "..self.type) error("build_event_data not implemented for "..self.type)
end, end,

View file

@ -1,7 +1,7 @@
local class = require("anselme.lib.class") local class = require("anselme.lib.class")
local fmt = require("anselme.common").fmt local fmt = require("anselme.common").fmt
local binser = require("anselme.lib.binser") 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 local unpack = table.unpack or unpack
-- NODES SHOULD BE IMMUTABLE AFTER CREATION IF POSSIBLE! -- NODES SHOULD BE IMMUTABLE AFTER CREATION IF POSSIBLE!

View file

@ -19,20 +19,20 @@
-- --
-- -- run the script -- -- run the script
-- while run_state:active() do -- while run_state:active() do
-- local e, data = run_state:step() -- local event, data = run_state:step()
-- if e == "text" then -- if event == "text" then
-- for _, l in ipairs(data) do -- for _, l in ipairs(data) do
-- print(l) -- print(l)
-- end -- end
-- elseif e == "choice" then -- elseif event == "choice" then
-- for i, l in ipairs(data) do -- for i, l in ipairs(data) do
-- print(("%s> %s"):format(i, l)) -- print(("%s> %s"):format(i, l))
-- end -- end
-- local choice = tonumber(io.read("l")) -- local choice = tonumber(io.read("l"))
-- data:choose(choice) -- data:choose(choice)
-- elseif e == "return" then -- elseif event == "return" then
-- run_state:merge() -- run_state:merge()
-- elseif e == "error" then -- elseif event == "error" then
-- error(data) -- error(data)
-- end -- end
-- end -- end

View file

@ -1,4 +1,4 @@
-- TODO: upstream -- NOTE: Modified to serialize function upvalues.
-- binser.lua -- binser.lua

394
anselme/lib/json.lua Normal file
View 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

View file

@ -1,5 +1,5 @@
local class = require("anselme.lib.class") 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 local Source
Source = class { Source = class {

View file

@ -1,6 +1,6 @@
local expression_to_ast = require("anselme.parser.expression.to_ast") local expression_to_ast = require("anselme.parser.expression.to_ast")
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 ast = require("anselme.ast")
local PartialScope, Block, Call, Identifier = ast.PartialScope, ast.Block, ast.Call, ast.Identifier local PartialScope, Block, Call, Identifier = ast.PartialScope, ast.Block, ast.Call, ast.Identifier

184
anselme/server/Client.lua Normal file
View 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
View 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
View 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

View 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

View 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

View 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

View file

@ -74,7 +74,7 @@ State = class {
--- Name of the branch associated to this State. --- Name of the branch associated to this State.
branch_id = "main", 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, source_branch = nil,
--- Return a new branch of this State. --- Return a new branch of this State.
@ -159,7 +159,7 @@ State = class {
-- Currently active script -- Currently active script
_coroutine = nil, _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) active = function(self)
return not not self._coroutine return not not self._coroutine
end, end,

View file

@ -65,13 +65,16 @@ return class {
if last_type then if last_type then
local last_buffer = state.scope:get(event_buffer_identifier) local last_buffer = state.scope:get(event_buffer_identifier)
local event_president = last_buffer:get(state, 1) -- elected representative of all concerned events 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) local data = event_president:build_event_data(state, last_buffer)
coroutine.yield(last_type, data)
-- clear room for the future -- clear room for the future
self:reset(state) self:reset(state)
-- post callback for _, event_data in ipairs(data) do
if event_president.post_flush_callback then event_president:post_flush_callback(state, last_buffer, data) end -- 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
end, end,
-- keep flushing until nothing is left (a flush may re-fill the buffer during its execution) -- keep flushing until nothing is left (a flush may re-fill the buffer during its execution)

View file

@ -16,7 +16,20 @@ return {
if l:truthy() then if l:truthy() then
return Boolean:new(env:defined(state, s:to_identifier())) return Boolean:new(env:defined(state, s:to_identifier()))
else 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
end end
}, },

View file

@ -1,6 +1,8 @@
-- TODO: doc in other language -- TODO: doc in other language
return [[ return [[
:@format = stdlib.format
:@bloc attaché = stdlib.attached block :@bloc attaché = stdlib.attached block
:@afficher = stdlib.print :@afficher = stdlib.print
@ -83,4 +85,6 @@ return [[
:@persister = stdlib.persist :@persister = stdlib.persist
:@écrire choix = stdlib.write choice :@écrire choix = stdlib.write choice
:@grouper texte par tag = stdlib.group text by tag
]] ]]

View file

@ -1,7 +1,7 @@
--- # Strings --- # Strings
-- @titlelevel 3 -- @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 ast = require("anselme.ast")
local String, Number = ast.String, ast.Number local String, Number = ast.String, ast.Number

View file

@ -9,6 +9,9 @@ local translation_manager = require("anselme.state.translation_manager")
local tag_manager = require("anselme.state.tag_manager") local tag_manager = require("anselme.state.tag_manager")
local resume_manager = require("anselme.state.resume_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 { return {
-- text -- text
{ {
@ -41,6 +44,37 @@ return {
end 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 -- choice
{ {
--- Write a choice event to the event buffer using this text and `fn` as the function to call if the choice is selected. --- Write a choice event to the event buffer using this text and `fn` as the function to call if the choice is selected.

View file

@ -21,20 +21,20 @@ run_state:run_file("script.ans")
-- run the script -- run the script
while run_state:active() do while run_state:active() do
local e, data = run_state:step() local event, data = run_state:step()
if e == "text" then if event == "text" then
for _, l in ipairs(data) do for _, l in ipairs(data) do
print(l) print(l)
end end
elseif e == "choice" then elseif event == "choice" then
for i, l in ipairs(data) do for i, l in ipairs(data) do
print(("%s> %s"):format(i, l)) print(("%s> %s"):format(i, l))
end end
local choice = tonumber(io.read("l")) local choice = tonumber(io.read("l"))
data:choose(choice) data:choose(choice)
elseif e == "return" then elseif event == "return" then
run_state:merge() run_state:merge()
elseif e == "error" then elseif event == "error" then
error(data) error(data)
end end
end end
@ -146,7 +146,7 @@ _defined at line 76 of [anselme/state/State.lua](../anselme/state/State.lua):_ `
### .source_branch ### .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,` _defined at line 78 of [anselme/state/State.lua](../anselme/state/State.lua):_ `source_branch = nil,`
@ -230,7 +230,7 @@ _defined at line 148 of [anselme/state/State.lua](../anselme/state/State.lua):_
### :active () ### :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)` _defined at line 163 of [anselme/state/State.lua](../anselme/state/State.lua):_ `active = function(self)`
@ -376,13 +376,13 @@ else
end end
``` ```
_defined at line 74 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `local TextEventData` _defined at line 87 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `local TextEventData`
### :group_by (tag_name) ### :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_name`. 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_name` tag and returns a list containing these groups. 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: For example, with the following Anselme script:
``` ```
@ -399,7 +399,7 @@ calling `text_event_data:group_by("speaker")` will return a list of three TextEv
* the second with the text "C"; with the tag `speaker="Lana"` * the second with the text "C"; with the tag `speaker="Lana"`
* the last with the text "D"; wiith the tag `speaker="John"` * the last with the text "D"; wiith the tag `speaker="John"`
_defined at line 96 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `group_by = function(self, tag_name)` _defined at line 109 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `group_by = function(self, tag_key)`
## ChoiceEventData ## ChoiceEventData
@ -467,13 +467,13 @@ A text will typically only consist of a single part unless it was built using te
Each text part is a table containing `text` (string) and `tags` (table) properties, for example: `{ text = "text part string", tags = { color = "red" } }`. 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 17 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `local LuaText` _defined at line 19 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `local LuaText`
### .raw ### .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. 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 25 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `raw = nil,` _defined at line 27 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `raw = nil,`
### :__tostring () ### :__tostring ()
@ -481,7 +481,7 @@ Returns a text representation of the LuaText, using Anselme's default formatting
Usage: `print(luatext)` Usage: `print(luatext)`
_defined at line 39 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `__tostring = function(self)` _defined at line 41 of [anselme/ast/Text.lua](../anselme/ast/Text.lua):_ `__tostring = function(self)`
--- ---
_file generated at 2024-11-11T13:33:43Z_ _file generated at 2024-11-17T15:00:50Z_

View file

@ -2,10 +2,11 @@
-- Behold! A documentation generator that doesn't try to be smart! -- Behold! A documentation generator that doesn't try to be smart!
-- Call this from the root anselme repository directory: `lua doc/gendocs.lua` -- 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 = { local files = {
"doc/api.md", "doc/api.md",
"doc/server.md",
"doc/standard_library.md" "doc/standard_library.md"
} }
local source_link_prefix = "../" local source_link_prefix = "../"

336
doc/server.md Normal file
View 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
View 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}}

View file

@ -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. 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 ! ### txt::is text !
Write a text event in the event buffer using this 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) ### tag (txt::is text, tags::is struct)
Create and return a new text from `text`, with the tags from `tags` added. 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=($()()))) ### 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. 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 ### original -> translated
Add a translation so `original` is replaced with `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 # 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 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 ### c::is environment . s::is string
Gets the variable named `s` defined in the environment `c`. 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 ### c::is environment . s::is string = v
Sets the variable named `s` defined in the environment `c` to `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 ### c::is environment . s::is symbol = v
Define a new variable `s` in the environment `c` with the value `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) ### import (env::is environment, symbol tuple::is tuple)
@ -1164,7 +1196,7 @@ import(env, [:a, :b])
:b = env.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) ### import (env::is environment, symbol::is symbol)
@ -1176,14 +1208,14 @@ import(env, :a)
:a = 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 (path::is string)
Load an Anselme script from a file and run it. Load an Anselme script from a file and run it.
Returns the environment containing the exported variables from the file. 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 # Typed values
@ -1336,4 +1368,4 @@ _defined at line 14 of [anselme/stdlib/wrap.lua](../anselme/stdlib/wrap.lua):_ `
--- ---
_file generated at 2024-11-09T16:02:36Z_ _file generated at 2024-11-17T15:00:50Z_

View file

@ -4,19 +4,13 @@ Loosely ordered by willingness to implement.
--- ---
Translation. Redundant `TextEventData:group_by` and `stdlib.group text by tag`: there can be only one.
Do some more fancy scope work to allow the translation to access variables defined in the translation file?
--- ---
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. Do some more fancy scope work to allow the translation to access variables defined in the translation file?
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/
--- ---

View 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. 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. 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. 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.