1227 lines
46 KiB
Lua
1227 lines
46 KiB
Lua
--[[ MARK: - Template Engine
|
|
|
|
Builds a formspec from the contents of a game-provided main menu file.
|
|
|
|
=================================================================================
|
|
Menu File Documentation
|
|
=================================================================================
|
|
|
|
Menu files are essentially just formspecs, but with special semantics and extended syntax.
|
|
|
|
First, menu files are made up of a number of dialog definitions. A dialog definition is
|
|
basically an HTML tag, where the tag name is the name of the dialog. For example this:
|
|
```
|
|
<main>
|
|
label[2,2;A label]
|
|
</main>
|
|
<other>
|
|
button[1,1;4,1;button;This is a button]
|
|
</other>
|
|
```
|
|
will define a dialog named 'main', with a label in it, and a dialog named 'other',
|
|
with a button in it. Every menu file must have a dialog named 'main', which serves
|
|
as the entry point of the game's menu.
|
|
|
|
Note that to prevent dialog definitions from conflicting with the contents of
|
|
hypertext[] elements, they must not have leading whitespace before the opening
|
|
delimiter.
|
|
|
|
Being able to define these other dialogs would be pretty pointless if they couldn't
|
|
be used for anything. Accordingly, you can use standard formspec actions, e.g. buttons,
|
|
to segue to a different dialog. To do this, set the action name to
|
|
'.show_dialog_<name>', where <name> is the name of the dialog you want to open.
|
|
You can also use '.overlay_dialog_<name>' to draw the target dialog on top of the
|
|
current dialog, and '.unoverlay_dialog' to hide it again.
|
|
|
|
There are several other action patterns that invoke special behavior:
|
|
-
|
|
- '.play': Start the game.
|
|
- '.play_with_world_named_<name>': Start the game on the specified world. If the
|
|
world in question does not exist, it will be created.
|
|
|
|
It is also possible to define metadata for the main menu. To do this, create a dialog
|
|
section named 'meta'. The contents of this section will then be parsed in the same way
|
|
as minetest.conf instead of being treated as a dialog.
|
|
|
|
Available metadata options are:
|
|
- 'enable_clouds': true/false (defaults to false)
|
|
|
|
|
|
Now, all this is good when we know exactly what content we want on the main menu.
|
|
But what if we want to render e.g. a list of worlds, or even a list of game modes?
|
|
This can be done using the @foreach construct. @foreach takes in a list of values,
|
|
the name of a JSON file, or a reference to a menu-exposed list.
|
|
|
|
The major advantage of a foreach loop, however, is interpolation. Inside a foreach
|
|
loop, the pattern '${<expression>}' will be replaces with the result of <expression>.
|
|
|
|
Inside an expression, you can reference variables as '@<name>'. Which variables are
|
|
available depends on the iterated list. In the case of a list literal, the current
|
|
item is exposed as a variable named @item, while for a JSON file, each key in the
|
|
file's top-level object will correspond to a variable. (Note that variable names may
|
|
only contain letters or numbers, even if mapped from a JSON file.)
|
|
|
|
The menu-provided lists are:
|
|
- @WORLDS: The list of worlds for this game. Exposes @name (the world name),
|
|
@path (the world's full absolute path), @gameid (the ID of the current game),
|
|
and @selected (whether the world is currently selected).
|
|
- @MODS: The list of installed mods that are not incompatible with this game.
|
|
Exposes @name, @title, @description, @author, @path, @depends, and @optional_depends.
|
|
- @WORLDMODS: The list of mods installed on the current world. No-op if no world
|
|
is selected.
|
|
|
|
Expressions also support rudimentary mathematical operations, namely addition (+),
|
|
'subtraction' (+-), multiplication (*), division (/), and exponentiation (^). Trying
|
|
to perform math on a non-tonumber()-able variable will treat the variable as 0.
|
|
|
|
As an example, this:
|
|
```
|
|
label[1,1;Worlds:]
|
|
@foreach:$WORLDS:name
|
|
label[1,${@i + 1};World named ${@name}]
|
|
@endforeach:name
|
|
|
|
@foreach:[one,two,three]:name2
|
|
label[4,${@i + 1};Item ${@i} is named: ${@item}]
|
|
@endforeach:name2
|
|
```
|
|
will:
|
|
- Create a "Worlds" label at 1,1;
|
|
- Create a label for every world with that world's name and a Y coordinate that
|
|
corresponds to the world's position in the list, plus 1 so these labels start
|
|
below the existing label.
|
|
- Create labels for "one", "two", and "three" that display the item's value and
|
|
its position in the list, with similarly increasing Y-coordinates.
|
|
|
|
Besides foreach loops, menu files also support conditionals. Conditionals are
|
|
written as '@if:<condition>:<name> ... @else:<name> ... @endif:<name>', where
|
|
<condition> is any expression. The conditional will be replaced with the contents
|
|
of its first block if the condition evaluates to non-zero, and the contents of
|
|
its second block otherwise. Note that the second block may not be omitted due to
|
|
how the parser works.
|
|
|
|
Example:
|
|
```
|
|
@if: @name = test :<name>
|
|
<formspec 1>
|
|
@else:<name>
|
|
<formspec 2>
|
|
@endif:<name>
|
|
```
|
|
|
|
Notes:
|
|
- The only uniqueness requirements for the name of a block is that it not be
|
|
the name of a statment of the same type contained in the body of that block.
|
|
This is so that the parser knows which `end` belongs to which block without
|
|
having to manage state.
|
|
|
|
Note: Because of the way the main menu works, image paths must be specified in full.
|
|
To make this non-painful, when referencing images use '$ASSET_PATH/<image name>'
|
|
instead of just the image name. $ASSET_PATH will be replaced with the actual path
|
|
to the game's menu/ directory, so images used by the main menu should be stored
|
|
there. You can use $ASSET_PATH in any context. Additionally, $DEFAULT_ASSET_PATH is
|
|
the path to the builtin assets folder, and $NO_IMAGE is the path to blank.png, in
|
|
case you need to stylistically unset a background image defined by a global style.
|
|
--]]
|
|
|
|
local zone = function() end
|
|
|
|
if not minetest then
|
|
zone = require("jit.zone")
|
|
minetest = {
|
|
formspec_escape = function() end,
|
|
hypertext_escape = function() end,
|
|
get_translator = function()
|
|
return function() end, function() end
|
|
end
|
|
}
|
|
local function basic_dump(o)
|
|
local tp = type(o)
|
|
if tp == "number" then
|
|
local s = tostring(o)
|
|
if tonumber(s) == o then
|
|
return s
|
|
end
|
|
-- Prefer an exact representation over a compact representation.
|
|
-- e.g. basic_dump(0.3) == "0.3",
|
|
-- but basic_dump(0.1 + 0.2) == "0.30000000000000004"
|
|
-- so the user can see that 0.1 + 0.2 ~= 0.3
|
|
return string.format("%.17g", o)
|
|
elseif tp == "string" then
|
|
return string.format("%q", o)
|
|
elseif tp == "boolean" then
|
|
return tostring(o)
|
|
elseif tp == "nil" then
|
|
return "nil"
|
|
elseif tp == "userdata" then
|
|
return tostring(o)
|
|
else
|
|
return string.format("<%s>", tp)
|
|
end
|
|
end
|
|
|
|
local keywords = {
|
|
["and"] = true,
|
|
["break"] = true,
|
|
["do"] = true,
|
|
["else"] = true,
|
|
["elseif"] = true,
|
|
["end"] = true,
|
|
["false"] = true,
|
|
["for"] = true,
|
|
["function"] = true,
|
|
["goto"] = true, -- Lua 5.2
|
|
["if"] = true,
|
|
["in"] = true,
|
|
["local"] = true,
|
|
["nil"] = true,
|
|
["not"] = true,
|
|
["or"] = true,
|
|
["repeat"] = true,
|
|
["return"] = true,
|
|
["then"] = true,
|
|
["true"] = true,
|
|
["until"] = true,
|
|
["while"] = true,
|
|
}
|
|
local function is_valid_identifier(str)
|
|
if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
--------------------------------------------------------------------------------
|
|
-- Dumps values in a line-per-value format.
|
|
-- For example, {test = {"Testing..."}} becomes:
|
|
-- _["test"] = {}
|
|
-- _["test"][1] = "Testing..."
|
|
-- This handles tables as keys and circular references properly.
|
|
-- It also handles multiple references well, writing the table only once.
|
|
-- The dumped argument is internal-only.
|
|
|
|
function dump2(o, name, dumped)
|
|
name = name or "_"
|
|
-- "dumped" is used to keep track of serialized tables to handle
|
|
-- multiple references and circular tables properly.
|
|
-- It only contains tables as keys. The value is the name that
|
|
-- the table has in the dump, eg:
|
|
-- {x = {"y"}} -> dumped[{"y"}] = '_["x"]'
|
|
dumped = dumped or {}
|
|
if type(o) ~= "table" then
|
|
return string.format("%s = %s\n", name, basic_dump(o))
|
|
end
|
|
if dumped[o] then
|
|
return string.format("%s = %s\n", name, dumped[o])
|
|
end
|
|
dumped[o] = name
|
|
-- This contains a list of strings to be concatenated later (because
|
|
-- Lua is slow at individual concatenation).
|
|
local t = {}
|
|
for k, v in pairs(o) do
|
|
local keyStr
|
|
if type(k) == "table" then
|
|
if dumped[k] then
|
|
keyStr = dumped[k]
|
|
else
|
|
-- Key tables don't have a name, so use one of
|
|
-- the form _G["table: 0xFFFFFFF"]
|
|
keyStr = string.format("_G[%q]", tostring(k))
|
|
-- Dump key table
|
|
t[#t + 1] = dump2(k, keyStr, dumped)
|
|
end
|
|
else
|
|
keyStr = basic_dump(k)
|
|
end
|
|
local vname = string.format("%s[%s]", name, keyStr)
|
|
t[#t + 1] = dump2(v, vname, dumped)
|
|
end
|
|
return string.format("%s = {}\n%s", name, table.concat(t))
|
|
end
|
|
|
|
|
|
-- This dumps values in a human-readable expression format.
|
|
-- If possible, the resulting string should evaluate to an equivalent value if loaded and executed.
|
|
-- For example, {test = {"Testing..."}} becomes:
|
|
-- [[{
|
|
-- test = {
|
|
-- "Testing..."
|
|
-- }
|
|
-- }]]
|
|
function dump(value, indent)
|
|
indent = indent or "\t"
|
|
local newline = indent == "" and "" or "\n"
|
|
|
|
local rope = {}
|
|
local write
|
|
do
|
|
-- Keeping the length of the table as a local variable is *much*
|
|
-- faster than invoking the length operator.
|
|
-- See https://gitspartv.github.io/LuaJIT-Benchmarks/#test12.
|
|
local i = 0
|
|
function write(str)
|
|
i = i + 1
|
|
rope[i] = str
|
|
end
|
|
end
|
|
|
|
local n_refs = {}
|
|
local function count_refs(val)
|
|
if type(val) ~= "table" then
|
|
return
|
|
end
|
|
local tbl = val
|
|
if n_refs[tbl] then
|
|
n_refs[tbl] = n_refs[tbl] + 1
|
|
return
|
|
end
|
|
n_refs[tbl] = 1
|
|
for k, v in pairs(tbl) do
|
|
count_refs(k)
|
|
count_refs(v)
|
|
end
|
|
end
|
|
count_refs(value)
|
|
|
|
local refs = {}
|
|
local cur_ref = 1
|
|
local function write_value(val, level)
|
|
if type(val) ~= "table" then
|
|
write(basic_dump(val))
|
|
return
|
|
end
|
|
|
|
local tbl = val
|
|
if refs[tbl] then
|
|
write(refs[tbl])
|
|
return
|
|
end
|
|
|
|
if n_refs[val] > 1 then
|
|
refs[val] = ("getref(%d)"):format(cur_ref)
|
|
write(("setref(%d)"):format(cur_ref))
|
|
cur_ref = cur_ref + 1
|
|
end
|
|
write("{")
|
|
if next(tbl) == nil then
|
|
write("}")
|
|
return
|
|
end
|
|
write(newline)
|
|
|
|
local function write_entry(k, v)
|
|
write(indent:rep(level))
|
|
write("[")
|
|
write_value(k, level + 1)
|
|
write("] = ")
|
|
write_value(v, level + 1)
|
|
write(",")
|
|
write(newline)
|
|
end
|
|
|
|
local keys = {string = {}, number = {}}
|
|
for k in pairs(tbl) do
|
|
local t = type(k)
|
|
if keys[t] then
|
|
table.insert(keys[t], k)
|
|
end
|
|
end
|
|
|
|
-- Write string-keyed entries
|
|
table.sort(keys.string)
|
|
for _, k in ipairs(keys.string) do
|
|
local v = val[k]
|
|
if is_valid_identifier(k) then
|
|
write(indent:rep(level))
|
|
write(k)
|
|
write(" = ")
|
|
write_value(v, level + 1)
|
|
write(",")
|
|
write(newline)
|
|
else
|
|
write_entry(k, v)
|
|
end
|
|
end
|
|
|
|
-- Write number-keyed entries
|
|
local len = 0
|
|
for i in ipairs(tbl) do
|
|
len = i
|
|
end
|
|
if #keys.number == len then -- table is a list
|
|
for _, v in ipairs(tbl) do
|
|
write(indent:rep(level))
|
|
write_value(v, level + 1)
|
|
write(",")
|
|
write(newline)
|
|
end
|
|
else -- table harbors arbitrary number keys
|
|
table.sort(keys.number)
|
|
for _, k in ipairs(keys.number) do
|
|
write_entry(k, tbl[k])
|
|
end
|
|
end
|
|
|
|
-- Write all remaining entries
|
|
for k, v in pairs(val) do
|
|
if not keys[type(k)] then
|
|
write_entry(k, v)
|
|
end
|
|
end
|
|
|
|
write(indent:rep(level - 1))
|
|
write("}")
|
|
end
|
|
write_value(value, 1)
|
|
return table.concat(rope)
|
|
end
|
|
function string.split(str, delim, include_empty, max_splits, sep_is_pattern)
|
|
delim = delim or ","
|
|
if delim == "" then
|
|
error("string.split separator is empty", 2)
|
|
end
|
|
max_splits = max_splits or -2
|
|
local items = {}
|
|
local pos, len = 1, #str
|
|
local plain = not sep_is_pattern
|
|
max_splits = max_splits + 1
|
|
repeat
|
|
local np, npe = string.find(str, delim, pos, plain)
|
|
np, npe = (np or (len+1)), (npe or (len+1))
|
|
if (not np) or (max_splits == 1) then
|
|
np = len + 1
|
|
npe = np
|
|
end
|
|
local s = string.sub(str, pos, np - 1)
|
|
if include_empty or (s ~= "") then
|
|
max_splits = max_splits - 1
|
|
items[#items + 1] = s
|
|
end
|
|
pos = npe + 1
|
|
until (max_splits == 0) or (pos > (len + 1))
|
|
return items
|
|
end
|
|
end
|
|
|
|
local fe = minetest.formspec_escape
|
|
local hte = minetest.hypertext_escape
|
|
|
|
template = {}
|
|
|
|
local operators = {
|
|
["or"] = {prec = 0, assoc = "left", kind = "binary"},
|
|
["and"] = {prec = 1, assoc = "left", kind = "binary"},
|
|
["=="] = {prec = 2, assoc = "left", kind = "binary"},
|
|
["<"] = {prec = 2, assoc = "left", kind = "binary"},
|
|
["<="] = {prec = 2, assoc = "left", kind = "binary"},
|
|
[">"] = {prec = 2, assoc = "left", kind = "binary"},
|
|
[">="] = {prec = 2, assoc = "left", kind = "binary"},
|
|
["+"] = {prec = 3, assoc = "left", kind = "binary"},
|
|
["-"] = {prec = 3, assoc = "left", kind = "binary"},
|
|
["*"] = {prec = 4, assoc = "left", kind = "binary"},
|
|
["/"] = {prec = 4, assoc = "left", kind = "binary"},
|
|
["%"] = {prec = 5, assoc = "left", kind = "binary"},
|
|
["not"] = {prec = 9, kind = "unary"},
|
|
["^"] = {prec = 10, assoc = "right", kind = "binary"},
|
|
-- Add more operators here, e.g. ["=="] = {prec = 0, assoc = "left"} for comparisons
|
|
}
|
|
|
|
function template.build_expr(expr)
|
|
|
|
-- First pass: Collect tokens into a flat list
|
|
local out = {}
|
|
local pos = 1
|
|
|
|
local len = #expr
|
|
|
|
while pos <= len do
|
|
local matched = false
|
|
|
|
|
|
-- Skip whitespace
|
|
local ws_start, ws_end = expr:find("^%s+", pos)
|
|
if ws_start then
|
|
pos = ws_end +1
|
|
matched = true
|
|
end
|
|
|
|
|
|
-- Strings
|
|
if not matched then
|
|
local str_start, str_end, str = expr:find('^"([^"]+)"', pos)
|
|
if not str_start then
|
|
str_start, str_end, str = expr:find("^'([^']+)'", pos)
|
|
end
|
|
if str_start then
|
|
out[#out +1] = {type = "string", value = str}
|
|
|
|
pos = str_end +1
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-- Numbers
|
|
if not matched then
|
|
local num_start, num_end, num = expr:find("^(%d+%.?%d*[eE]?[+-]?%d*)", pos)
|
|
if num_start then
|
|
out[#out +1] = {type = "number", value = tonumber(num)}
|
|
|
|
pos = num_end +1
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-- Operators
|
|
if not matched then
|
|
for op in pairs(operators) do
|
|
local op_start = pos
|
|
local op_end = pos +op:len() -1
|
|
if expr:sub(op_start, op_end) == op then
|
|
out[#out +1] = {type = "operator", op = op}
|
|
|
|
pos = op_end +1
|
|
matched = true
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-- Grouping operators
|
|
if not matched then
|
|
local op_start, op_end, op = expr:find("^(%p)", pos)
|
|
if op_start then
|
|
if op == "," then
|
|
out[#out +1] = {type = "comma"}
|
|
elseif op == "." then
|
|
out[#out +1] = {type = "dot"}
|
|
elseif op == "(" then
|
|
out[#out +1] = {type = "lparen"}
|
|
elseif op == ")" then
|
|
out[#out +1] = {type = "rparen"}
|
|
elseif op == "[" then
|
|
out[#out +1] = {type = "lbracket"}
|
|
elseif op == "]" then
|
|
out[#out +1] = {type = "rbracket"}
|
|
else
|
|
goto aaa
|
|
end
|
|
|
|
pos = op_end +1
|
|
matched = true
|
|
::aaa::
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-- Keywords
|
|
if not matched then
|
|
if expr:find("^true") then
|
|
out[#out +1] = {type = "bool", value = true}
|
|
pos = pos +3
|
|
matched = true
|
|
elseif expr:find("^false") then
|
|
out[#out +1] = {type = "bool", value = false}
|
|
pos = pos +4
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-- Identifiers
|
|
if not matched then
|
|
local id_start, id_end, id = expr:find("^([%a_][%w_]*)", pos)
|
|
if id_start then
|
|
local item = {type = "identifier", name = id}
|
|
|
|
pos = id_end +1
|
|
out[#out +1] = item
|
|
matched = true
|
|
end
|
|
end
|
|
|
|
|
|
|
|
if not matched then
|
|
pos = pos +1
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Second pass: Fold the flat list into an expression tree
|
|
local tree = {}
|
|
local cursor = 1
|
|
|
|
local peek, consume, expect, parse_expr, parse_primary, parse_postfix
|
|
|
|
function peek()
|
|
return out[cursor]
|
|
end
|
|
function consume()
|
|
cursor = cursor +1
|
|
return out[cursor -1]
|
|
end
|
|
|
|
function expect(ty)
|
|
local t = consume()
|
|
if not t or t.type ~= ty then
|
|
error("Expected " .. ty .. " but got " .. (t and t.type or "nil").."(in expression `"..expr.."`)")
|
|
end
|
|
end
|
|
|
|
parse_expr = function(min_prec)
|
|
min_prec = min_prec or 0
|
|
|
|
-- Parse left operand: handle unary prefix operators
|
|
local left
|
|
local token = peek()
|
|
if token and token.type == "operator" then
|
|
local op_info = operators[token.op]
|
|
if op_info and op_info.kind == "unary" then
|
|
-- For unary prefix, use a high binding power
|
|
consume()
|
|
-- Recurse with high precedence for operand
|
|
local operand = parse_expr(op_info.prec)
|
|
left = {type = "unary_operator", op = token.op, expr = operand}
|
|
else
|
|
left = parse_primary()
|
|
end
|
|
else
|
|
left = parse_primary()
|
|
end
|
|
|
|
-- Apply postfix operators (index, call) - highest precedence
|
|
left = parse_postfix(left)
|
|
|
|
-- Handle binary operators (infix)
|
|
while true do
|
|
local token = peek()
|
|
if not token or token.type ~= "operator" then break end
|
|
local op_info = operators[token.op]
|
|
if not op_info or op_info.kind ~= "binary" or op_info.prec < min_prec then break end
|
|
|
|
consume()
|
|
local next_min_prec = op_info.prec
|
|
if op_info.assoc == "left" then
|
|
next_min_prec = next_min_prec + 1
|
|
end
|
|
-- For right-assoc, use >= so same prec binds right
|
|
local right = parse_expr(next_min_prec)
|
|
left = {type = "binary_operator", op = token.op, lhs = left, rhs = right}
|
|
end
|
|
|
|
return left
|
|
end
|
|
|
|
-- Parse postfix operators (index, call)
|
|
parse_postfix = function(left)
|
|
while peek() do
|
|
local t = peek()
|
|
if t.type == "lbracket" then
|
|
-- Indexing has high precedence
|
|
consume()
|
|
local key = parse_expr(0) -- Inside [] can have full expressions
|
|
expect("rbracket")
|
|
left = {type = "index", base = left, key = key}
|
|
elseif t.type == "lparen" then
|
|
-- Function call
|
|
consume()
|
|
local args = {}
|
|
while peek().type ~= "rparen" do
|
|
local e = parse_expr(0)
|
|
args[#args +1] = e
|
|
if peek().type == "comma" then consume() end
|
|
end
|
|
expect("rparen")
|
|
left = {type = "call", func = left, args = args}
|
|
elseif t.type == "dot" then
|
|
consume()
|
|
if peek().type == "identifier" then
|
|
left = {type = "index", base = left, key = {type = "string", value = consume().name}}
|
|
else -- Syntax error
|
|
|
|
end
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
return left
|
|
end
|
|
|
|
parse_primary = function()
|
|
local t = peek()
|
|
if t.type == "number" or t.type == "string" or t.type == "bool" or t.type == "list" then
|
|
consume()
|
|
return t
|
|
elseif t.type == "identifier" then
|
|
consume()
|
|
-- if peek() and peek().type == "lparen" then
|
|
-- consume()
|
|
-- local args = {}
|
|
-- while peek().type ~= "rparen" do
|
|
-- local e = parse_expr(0)
|
|
-- args[#args +1] = e
|
|
-- if peek().type == "comma" then consume() end
|
|
-- end
|
|
--
|
|
-- return {type = "call", func = t, args = args}
|
|
-- end
|
|
return t
|
|
elseif t.type == "lbracket" then
|
|
consume()
|
|
local list = {}
|
|
while peek().type ~= "rbracket" do
|
|
local e = parse_expr(0)
|
|
list[#list +1] = e
|
|
if peek().type == "comma" then consume() end
|
|
end
|
|
return {type = "list", value = list}
|
|
elseif t.type == "lparen" then
|
|
consume()
|
|
local e = parse_expr(0)
|
|
expect("rparen")
|
|
return e
|
|
else
|
|
print("Unexpected token: "..consume().type)
|
|
end
|
|
|
|
end
|
|
|
|
|
|
return parse_expr(0)
|
|
end
|
|
|
|
function template.build(menu)
|
|
local _id = 0
|
|
local function id()
|
|
_id = _id +1
|
|
return _id
|
|
end
|
|
|
|
local out = {["@root"] = {}}
|
|
local view_name = "main"
|
|
local block = {parent = {}, id="_root"}
|
|
local item = {parent = {}, id="_root"}
|
|
local offset = 1
|
|
local pos = 1
|
|
while true do
|
|
local matched = false
|
|
local stmt_start, stmt_end, stmt = menu:find("{%%%s*(.-)%s*%%}", pos)
|
|
local expr_start, expr_end, expr = menu:find("{{(.-)}}", pos)
|
|
local comment_start, comment_end, comment = menu:find("{#(.-)#}", pos)
|
|
|
|
-- Statements
|
|
if stmt_start and (not expr_start or stmt_start < expr_start) and (not comment_start or stmt_start < comment_start) then
|
|
|
|
block[#block +1] = menu:sub(offset, stmt_start -1)
|
|
local verb = stmt:match "^(%a+)"
|
|
if verb == "if" then
|
|
item = {
|
|
id = id(),
|
|
type = "if",
|
|
parent = block,
|
|
conditions = {{condition = template.build_expr(stmt:match "if%s*(.*)"), body = {}}}
|
|
}
|
|
block[#block +1] = item
|
|
block = item.conditions[1].body
|
|
block.parent = item
|
|
|
|
if not block.parent then print("block.parent set to nil: "..menu:sub(stmt_start -50, stmt_end +50)) end
|
|
elseif verb == "elseif" then
|
|
local x = {
|
|
condition = template.build_expr(stmt:match "if%s*(.*)"),
|
|
body = {}
|
|
}
|
|
item.conditions[#item.conditions +1] = x
|
|
block = x.body
|
|
block.parent = item
|
|
|
|
if not block.parent then print("block.parent set to nil: "..menu:sub(stmt_start -50, stmt_end +50)) end
|
|
elseif verb == "else" then
|
|
local x = {
|
|
condition = {type = "bool", value = true},
|
|
body = {}
|
|
}
|
|
item.conditions[#item.conditions +1] = x
|
|
block = x.body
|
|
block.parent = item
|
|
|
|
if not block.parent then print("block.parent set to nil: "..menu:sub(stmt_start -50, stmt_end +50)) end
|
|
elseif verb == "endif" then
|
|
block.parent = nil
|
|
block = item.parent
|
|
item.parent = nil
|
|
item = block.parent
|
|
elseif verb == "for" then
|
|
local var, iter = stmt:match "for%s*([%a_][%w_]*)%s*in%s*(.*)"
|
|
item = {
|
|
id = id(),
|
|
type = "for",
|
|
parent = block,
|
|
varname = var,
|
|
iterate = template.build_expr(iter),
|
|
body = {}
|
|
}
|
|
block[#block +1] = item
|
|
block = item.body
|
|
block.parent = item
|
|
elseif verb == "endfor" then
|
|
block.parent = nil
|
|
block = item.parent
|
|
item.parent = nil
|
|
item = block.parent
|
|
elseif verb == "macro" then
|
|
local name, params = stmt:match "macro%s*([%a_][%w_]*)%s*(.*)"
|
|
item = {
|
|
type = "macro",
|
|
parent = block,
|
|
name = name,
|
|
params = params:match("^%((.*)%)$"):split("%s*,%s*", false, -1, true),
|
|
body = {}
|
|
}
|
|
block[#block +1] = item
|
|
block = item.body
|
|
block.parent = item
|
|
elseif verb == "endmacro" then
|
|
block.parent = nil
|
|
block = item.parent
|
|
item.parent = nil
|
|
item = block.parent
|
|
elseif verb == "set" then
|
|
local name, expr = stmt:match "set %s*([%a_][%w_]*)%s*=?%s*(.*)"
|
|
if expr and expr ~= "" then
|
|
block[#block +1] = {type = "set", name = name, value = template.build_expr(expr)}
|
|
else
|
|
item = {
|
|
type = "set_block",
|
|
parent = block,
|
|
name = name,
|
|
value = {}
|
|
}
|
|
block[#block +1] = item
|
|
block = item.value
|
|
block.parent = item
|
|
end
|
|
elseif verb == "endset" then
|
|
block = item.parent
|
|
item.parent = nil
|
|
item = block.parent
|
|
block.parent = nil
|
|
elseif verb == "include" then
|
|
|
|
elseif verb == "view" then
|
|
view_name = stmt:match "view%s*([%a_][%w_]*)%s*" or "main"
|
|
table.insert_all(out["@root"], block)
|
|
item = {id = "root"}
|
|
block = {id = "root"}
|
|
elseif verb == "endview" then
|
|
out[view_name] = block
|
|
block = {}
|
|
end
|
|
|
|
offset = stmt_end +1
|
|
pos = stmt_end
|
|
matched = true
|
|
|
|
end
|
|
|
|
-- Interpolations
|
|
if expr_start and expr_start < (stmt_start or math.huge) and expr_start < (comment_start or math.huge) then
|
|
block[#block +1] = menu:sub(offset, expr_start -1)
|
|
|
|
block[#block +1] = template.build_expr(expr)
|
|
|
|
offset = expr_end +1
|
|
pos = expr_end
|
|
matched = true
|
|
end
|
|
|
|
-- Comments
|
|
if comment_start and comment_start < (stmt_start or math.huge) and comment_start < (expr_start or math.huge) then
|
|
block[#block +1] = menu:sub(offset, comment_start -1)
|
|
offset = comment_end +1
|
|
pos = comment_end
|
|
matched = true
|
|
end
|
|
|
|
|
|
if not matched then
|
|
break
|
|
end
|
|
end
|
|
|
|
block[#block +1] = menu:sub(offset)
|
|
out[#out +1] = block
|
|
|
|
-- Attach a reference to the root scope in each view.
|
|
for k, v in pairs(out) do
|
|
if k ~= "@root" then
|
|
v.root = out["@root"]
|
|
end
|
|
end
|
|
|
|
return out
|
|
end
|
|
|
|
function template.evaluate_expr(expr, vars)
|
|
assert(expr, "No expression given!")
|
|
if expr.type == "binary_operator" then
|
|
assert(expr.lhs and expr.rhs, "Binary operator missing an operand")
|
|
if expr.op == "+" then
|
|
return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) + (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
|
|
elseif expr.op == "*" then
|
|
return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) * (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
|
|
elseif expr.op == "-" then
|
|
return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) - (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
|
|
elseif expr.op == "/" then
|
|
return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) / (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
|
|
elseif expr.op == "%" then
|
|
return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) % (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
|
|
elseif expr.op == "or" then
|
|
return template.evaluate_expr(expr.lhs, vars) or template.evaluate_expr(expr.rhs, vars)
|
|
elseif expr.op == "and" then
|
|
return template.evaluate_expr(expr.lhs, vars) and template.evaluate_expr(expr.rhs, vars)
|
|
elseif expr.op == "==" then
|
|
return template.evaluate_expr(expr.lhs, vars) == template.evaluate_expr(expr.rhs, vars)
|
|
elseif expr.op == ">" then
|
|
return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) > (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
|
|
elseif expr.op == ">" then
|
|
return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) >= (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
|
|
elseif expr.op == "<" then
|
|
return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) < (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
|
|
elseif expr.op == "<=" then
|
|
return (tonumber(template.evaluate_expr(expr.lhs, vars)) or 0) <= (tonumber(template.evaluate_expr(expr.rhs, vars)) or 0)
|
|
else
|
|
return template.evaluate_expr(expr.lhs, vars)
|
|
end
|
|
elseif expr.type == "unary_operator" then
|
|
assert(expr.expr, "Invalid unary expression")
|
|
if expr.op == "not" then
|
|
return not template.evaluate_expr(expr.expr, vars)
|
|
else
|
|
return template.evaluate_expr(expr.expr, vars)
|
|
end
|
|
elseif expr.type == "identifier" then
|
|
return vars[expr.name]
|
|
elseif expr.type == "index" then
|
|
assert(expr.base and expr.key, "Invalid indexing")
|
|
return template.evaluate_expr(expr.base, vars)[template.evaluate_expr(expr.key, vars)]
|
|
elseif expr.type == "call" then
|
|
assert(expr.func, "Invalid function ref")
|
|
local fn = template.evaluate_expr(expr.func, vars)
|
|
if type(fn) == "function" then
|
|
local args = {}
|
|
for _, x in pairs(expr.args) do
|
|
assert(x, "Invalid argument")
|
|
args[#args +1] = template.evaluate_expr(x, vars)
|
|
end
|
|
return fn(unpack(args))
|
|
end
|
|
elseif expr.type == "list" then
|
|
local out = {}
|
|
for i, x in ipairs(expr.value) do
|
|
assert(x, "Invalid list item")
|
|
out[i] = template.evaluate_expr(x, vars)
|
|
end
|
|
return out
|
|
else
|
|
return expr.value or false
|
|
end
|
|
end
|
|
|
|
|
|
local S, PS = minetest.get_translator()
|
|
local env = {
|
|
S = S,
|
|
translate = S,
|
|
PS = S,
|
|
translate_n = PS,
|
|
fe = fe,
|
|
formspec_escape = fe,
|
|
hte = hte,
|
|
hypertext_escape = hte,
|
|
DEFAULT_ASSETS = assets,
|
|
floor = math.floor,
|
|
ceil = math.ceil,
|
|
round = math.round,
|
|
min = math.min,
|
|
max = math.max,
|
|
-- Convert arguments to a formspec-safe representation that can be evaluated in on_reveive_fields
|
|
action = function(type, ...)
|
|
local args = {...}
|
|
if type == "set" then
|
|
local name = args[1]
|
|
return ".set_|"..fe(name).."|_to_"..fe(minetest.write_json(args[2]))
|
|
end
|
|
return out
|
|
end,
|
|
cycle = function(idx, ...)
|
|
local options = {...}
|
|
return options[((idx -1) %#options) +1]
|
|
end,
|
|
upper = function(str)
|
|
return string.upper(str)
|
|
end,
|
|
len = function(x)
|
|
return #x
|
|
end,
|
|
range = function(min, max, step)
|
|
local out = {}
|
|
if not step then step = 1 end
|
|
for i = min, max, step do
|
|
out[#out +1] = i
|
|
end
|
|
return out
|
|
end
|
|
}
|
|
|
|
function template.evaluate(dialog, vars, depth)
|
|
-- Ensure that variables from `env` remain accessible even when `vars` has its own metatable set.
|
|
local out
|
|
if not depth then
|
|
local real_vars = vars
|
|
vars = setmetatable({}, {
|
|
__index = function(tbl, k)
|
|
return real_vars[k] or env[k]
|
|
end
|
|
})
|
|
depth = 0
|
|
out = dialog.root and {template.evaluate(dialog.root, vars, depth +1)} or {}
|
|
else
|
|
out = {}
|
|
end
|
|
for _, item in ipairs(dialog) do
|
|
if type(item) == "string" then
|
|
out[#out +1] = item
|
|
else
|
|
if item.type == "if" then
|
|
for _, cond in ipairs(item.conditions) do
|
|
if template.evaluate_expr(cond.condition, vars) then
|
|
out[#out +1] = template.evaluate(cond.body, vars, depth +1)
|
|
break
|
|
end
|
|
end
|
|
elseif item.type == "for" then
|
|
local iterable = template.evaluate_expr(item.iterate, vars)
|
|
if type(iterable) == "table" then
|
|
for i, entry in ipairs(iterable) do
|
|
local vars2 = {
|
|
index = i,
|
|
index0 = i -1,
|
|
length = #iterable,
|
|
first = i == 1,
|
|
depth = 1,
|
|
previtem = iterable[i -1],
|
|
nextitem = iterable[i +1],
|
|
cycle = function(...)
|
|
return env.cycle(i, ...)
|
|
end
|
|
}
|
|
vars2.last = i == vars2.length
|
|
vars2.revindex = vars2.length -vars2.index
|
|
vars2.revindex0 = vars2.length -vars2.index0
|
|
if vars.loop and vars.loop.depth then
|
|
vars.depth = vars.loop.depth +1
|
|
end
|
|
-- Save the original values of these variables, to simulate a closure.
|
|
local prev_var = vars[item.varname]
|
|
local prev_loop = vars.loop
|
|
vars[item.varname] = entry
|
|
vars.loop = vars2
|
|
out[#out +1] = template.evaluate(item.body, vars, depth +1)
|
|
-- Restore the variables we set to their original values.
|
|
vars[item.varname] = prev_var
|
|
vars.loop = prev_loop
|
|
end
|
|
end
|
|
elseif item.type == "set" then
|
|
vars[item.name] = template.evaluate_expr(item.value, vars)
|
|
elseif item.type == "set_block" then
|
|
vars[item.name] = template.evaluate(item.value, vars, depth +1)
|
|
elseif item.type == "macro" then
|
|
vars[item.name] = function(...)
|
|
local argv = {...}
|
|
local args = {}
|
|
for i, x in ipairs(argv) do
|
|
args[item.params[i]] = x
|
|
end
|
|
return template.evaluate(item.body, setmetatable(args, {__index = vars}), depth +1)
|
|
end
|
|
else
|
|
out[#out + 1] = tostring(template.evaluate_expr(item, vars))
|
|
end
|
|
end
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
|
|
function template.test()
|
|
-- local profile = require("jit.profile")
|
|
|
|
-- local profile_data = {}
|
|
-- local function profile_callback(thread, samples, vmstate)
|
|
-- profile_data[#profile_data +1] = {profile.dumpstack(thread, "pF;", -100), vmstate,
|
|
-- " ", samples, "\n"}
|
|
-- end
|
|
|
|
-- Start profiling
|
|
-- profile.start("li0.1", profile_callback)
|
|
|
|
local ast = template.build [[
|
|
{% macro field(x, y, w, h, name, value) %}
|
|
image[{{ x }},{{ y }};{{ w }},{{ h }};{{ DEFAULT_ASSETS }}btn_bg_2_light.png;8,8]
|
|
field[{{ x + 0.1 }},{{ y }};{{ w - 0.2 }},{{ h }};{{ name }};;{{ value }}]
|
|
{% endmacro %}
|
|
{% view main %}
|
|
|
|
{% if selected_world %}
|
|
{% set list_width = WIDTH * 0.3 %}
|
|
{% else %}
|
|
{% set list_width = WIDTH * 0.8 %}
|
|
{% endif %}
|
|
|
|
{{ field(2, 2, 4, 0.75, 'test', '') }}
|
|
|
|
{% set col = 12 % 3 %}
|
|
{{ col > 0 and 'yes' or 'no' }}
|
|
|
|
{{ action("set", "q", [1, 2, 3]) }}
|
|
|
|
scroll_container[0,0;{{ foo }},{{ foo }};worldscroll;vertical;;1,0]
|
|
|
|
{#
|
|
image[{{ WIDTH * 0.1 - 0.1 }},{{ HEIGHT * 0.1 - 0.1}};{{ WIDTH * 0.8 + 0.2 }},{{ HEIGHT * 0.8 + 0.2 }};{{ DEFAULT_ASSETS }}bg_translucent.png;8,8]
|
|
scroll_container[{{ WIDTH * 0.1 }},{{ HEIGHT * 0.1 }};{{ list_width }},{{ HEIGHT * 0.8 }};worldscroll;vertical;;0,0]
|
|
{% for world in get_worlds() %}
|
|
style[.select_world_{{ world.path }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
|
|
style[.select_world_{{ world.path }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
|
|
style[.select_world_{{ world.path }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
|
|
|
|
button[0,{{ loop.index0 * 0.5}};{{ list_width }},0.5;.select_world_{{ world.path }};{{ world.name }}]
|
|
{% endfor %}
|
|
scroll_container_end[]
|
|
|
|
{% if selected_world %}
|
|
box[{{ WIDTH * 0.4 - 0.05 }},{{ HEIGHT * 0.1 - 0.1 }};0.1,{{ HEIGHT * 0.8 + 0.2 }};#292d2fff]
|
|
scroll_container[{{ WIDTH * 0.4 + 0.05 }},{{ HEIGHT * 0.1 }};{{ WIDTH * 0.8 - 0.05 }},{{ HEIGHT * 0.8 }};worlconfigscroll;vertical;;0,0]
|
|
{% set j = 0 %}
|
|
|
|
{% if setting_damage %}
|
|
{% if damage_enabled %}
|
|
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_damage_enabled_to_false;]
|
|
{% else %}
|
|
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_damage_enabled_to_true;]
|
|
{% endif %}
|
|
label[0.5,{{ j + 0.25 }};Damage]
|
|
{% set j = j + 0.5 %}
|
|
{% endif %}
|
|
|
|
{% if setting_creative %}
|
|
{% if creative_enabled %}
|
|
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_creative_enabled_to_false;]
|
|
{% else %}
|
|
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_creative_enabled_to_true;]
|
|
{% endif %}
|
|
label[0.5,{{ j + 0.25 }};Creative]
|
|
{% set j = j + 0.5 %}
|
|
{% endif %}
|
|
|
|
{% set play_str = "Play" %}
|
|
|
|
{% if setting_server %}
|
|
{% if server_enabled %}
|
|
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_filled.png;.set_server_enabled_to_false;]
|
|
{% set play_str = "Host server" %}
|
|
{% else %}
|
|
image_button[0,{{ j }};0.5,0.5;{{ DEFAULT_ASSETS }}checkbox_empty.png;.set_server_enabled_to_true;]
|
|
{% set play_str = "Play" %}
|
|
{% endif %}
|
|
|
|
label[0.5,{{ j + 0.25 }};Server]
|
|
{% set j = j + 0.5 %}
|
|
{% endif %}
|
|
|
|
{% set j = j + 1 %}
|
|
|
|
button[{{ WIDTH * 0.05 }},{{ j }};{{ WIDTH * 0.4 }},0.75;.overlay_view_modconfig;Configure mods]
|
|
button[{{ WIDTH * 0.05 }},{{ j + 1 }};{{ WIDTH * 0.4 }},0.75;.play;{{ play_str }}]
|
|
|
|
scroll_container_end[]
|
|
scrollbar[-800,6;0,2;vertical;worldconfigscroll;]
|
|
{% endif %}
|
|
scrollbaroptions[arrows=hide]
|
|
scrollbar[-800,6;0,2;vertical;worldscroll;]
|
|
|
|
{% endview %}
|
|
{% view modconfig %}
|
|
|
|
image[{{ WIDTH * 0.05 - 0.1 }},{{ HEIGHT * 0.05 - 0.1 }};{{ WIDTH * 0.4 + 0.2 }},{{ HEIGHT * 0.9 - 0.75 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8]
|
|
hypertext[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.4 }},0.75;;<global valign=middle halign=center><b>Available mods</b>]
|
|
scroll_container[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 + 0.75 }};{{ WIDTH * 0.4 }},{{ HEIGHT * 0.9 - 1.7 }};modsscroll;vertical;;0,0]
|
|
{% for mod in get_mods() %}
|
|
style[.select_mod_{{ mod.name }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
|
|
style[.select_mod_{{ mod.name }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
|
|
style[.select_mod_{{ mod.name }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
|
|
button[0,{{ loop.index0 * 0.5 }};{{ WIDTH * 0.4 }},0.5;.select_mod_{{ mod.name }};{{ mod.name }}]
|
|
{% endfor %}
|
|
scroll_container_end[]
|
|
scrollbar[-800,6;0,2;vertical;modsscroll;]
|
|
|
|
image[{{ WIDTH * 0.55 - 0.1 }},{{ HEIGHT * 0.05 - 0.1 }};{{ WIDTH * 0.4 + 0.2 }},{{ HEIGHT * 0.9 - 0.75 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8]
|
|
hypertext[{{ WIDTH * 0.55 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.4 }},0.75;;<global valign=middle halign=center><b>Enabled mods</b>]
|
|
scroll_container[{{ WIDTH * 0.55 }},{{ HEIGHT * 0.05 + 0.75 }};{{ WIDTH * 0.4 }},{{ HEIGHT * 0.9 - 1.7 }};worldmodsscroll;vertical;;0,0]
|
|
{% for mod in get_world_mods() %}
|
|
{% if not mod.game_provided %}
|
|
style[.select_mod_{{ mod.name }};bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
|
|
style[.select_mod_{{ mod.name }}:hovered;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
|
|
style[.select_mod_{{ mod.name }}:hovered+focused;bgimg={{ DEFAULT_ASSETS }}white.png;bgimg_middle=0;bgcolor={{ loop.cycle('#373530', '#403e39') }}]
|
|
button[0,{{ loop.index0 * 0.5 }};{{ WIDTH * 0.4 }},0.5;.select_mod_{{ mod.name }};{{ mod.name }}]
|
|
{% endif %}
|
|
{% endfor %}
|
|
scroll_container_end[]
|
|
scrollbar[-800,6;0,2;vertical;worldmodsscroll;]
|
|
|
|
image_button[{{ WIDTH * 0.45 }},{{ HEIGHT * 0.5 - WIDTH * 0.1 }};{{ WIDTH * 0.1 }},{{ WIDTH * 0.1 }};{{ DEFAULT_ASSETS }}arrow_right.png;.add_mod_to_world;]
|
|
image_button[{{ WIDTH * 0.45 }},{{ HEIGHT * 0.5 }};{{ WIDTH * 0.1 }},{{ WIDTH * 0.1 }};{{ DEFAULT_ASSETS }}arrow_left.png;.remove_mod_from_world;]
|
|
tooltip[.add_mod_to_world;Add mod to world;#444;#aaa]
|
|
tooltip[.remove_mod_from_world;Remove mod from world;#444;#aaa]
|
|
#}
|
|
|
|
button[{{ WIDTH * 0.1 }},{{ HEIGHT * 0.95 - 0.75 }};{{ WIDTH * 0.4 }},0.75;.unoverlay_view;Cancel]
|
|
button[{{ WIDTH * 0.5 }},{{ HEIGHT * 0.95 - 0.75 }};{{ WIDTH * 0.4 }},0.75;.unoverlay_view;Confirm]
|
|
|
|
{% endview %}
|
|
{% view addworld %}
|
|
|
|
{% endview %}
|
|
]]
|
|
|
|
-- print(dump(ast, " "))
|
|
|
|
print(template.evaluate(ast.main, setmetatable({z = false, bar = true, foo = 3, blah = "test"}, {__index = {q = "hello"}})))
|
|
|
|
-- profile.stop()
|
|
--
|
|
-- print("Profiler data: "..dump(profile_data, " "))
|
|
--
|
|
-- -- Analyze results
|
|
-- for stack, samples in pairs(profile_data) do
|
|
-- print(string.format("%s: %d samples", stack, samples))
|
|
-- end
|
|
end
|
|
|
|
local time = os.clock()
|
|
--for _ = 1, 1000 do
|
|
template.test()
|
|
--end
|
|
print("Execution finished in: "..(os.clock() -time))
|