1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-27 16:49:31 +00:00

[api] add server mode

This commit is contained in:
Étienne Fildadut 2024-11-17 16:07:18 +01:00
parent 77c6ac6ba2
commit 49c9741349
24 changed files with 1553 additions and 52 deletions

View file

@ -21,20 +21,20 @@ 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)
end
elseif e == "choice" then
elseif event == "choice" then
for i, l in ipairs(data) do
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
@ -146,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,`
@ -230,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)`
@ -376,13 +376,13 @@ else
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:
```
@ -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 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
@ -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" } }`.
_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
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 ()
@ -481,7 +481,7 @@ Returns a text representation of the LuaText, using Anselme's default formatting
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!
-- 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 = "../"

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}}