diff --git a/init.lua b/init.lua
index f1c46ba..13e7627 100644
--- a/init.lua
+++ b/init.lua
@@ -71,7 +71,7 @@ minetest.set_formspec_prepend("\
style_type[image_button;border=false]\
style_type[button;border=false;bgimg="..assets.."btn_bg_2.png;bgimg_middle=8,8]\
style_type[button:hovered;border=false;bgimg="..assets.."btn_bg_2_hover.png;bgimg_middle=8,8]\
- style[nobg,nobg:hovered,nobg:focused,nobg:hovered+focused;border=false;bgimg="..default_textures.."blank.png;bgimg_middle=0]\
+ style[nobg,nobg:hovered,nobg:focused,nobg:hovered+focused;border=false;bgimg="..default_textures.."blank.png;bgimg_middle=0;content_offset=0,0]\
")
local meta_header = "formspec_version[8]\
@@ -101,150 +101,208 @@ local content_header = "formspec_version[8]\
"
local default_game_menu = [[
-
- enable_clouds = true
-
-
- @if:@selected_world:fi2
- @set:list_width:@WIDTH * 0.3
- @else:fi2
- @set:list_width:@WIDTH * 0.8
- @endif:fi2
+ {% 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 %}
- label[0,0;{{ selected_world }}]
+ {% set worlds = get_worlds() %}
- image[${@WIDTH * 0.1 +-0.1},${@HEIGHT * 0.1 +-0.1};${@WIDTH * 0.8 + 0.2},${@HEIGHT * 0.8 + 0.2};$DEFAULT_ASSET_PATH/bg_translucent.png;8,8]
- scroll_container[${@WIDTH * 0.1},${@HEIGHT * 0.1};${@list_width},${@HEIGHT * 0.8};worldscroll;vertical;;0,0]
- @foreach:@WORLDS:worlds
- @if:@i % 2:sel
- style[.select_world_${@path};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
- style[.select_world_${@path}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
- style[.select_world_${@path}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
- @else:sel
- style[.select_world_${@path};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
- style[.select_world_${@path}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
- style[.select_world_${@path}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
- @endif:sel
- button[0,${(@i +-1) * 0.5};${@list_width},0.5;.select_world_${@path};${@name}]
- @endforeach:worlds
+ {{ field(2, 2, 4, 0.75, 'test', '???') }}
+
+ {# Special layouts are applied when there are less than four worlds for a given game. #}
+ {% if len(worlds) == 1 %}
+
+ image[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 - 2 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 - 1.5 }},{{ HEIGHT * 0.5 - 1.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
+ button[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 + 1 }};4,0.75;nobg;{{ worlds[1].name }}]
+
+ {% elseif len(worlds) == 2 %}
+
+ image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 - 2 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 - 1.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
+ button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 1 }};4,0.75;nobg;{{ worlds[1].name }}]
+
+ image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 - 2 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 - 1.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[2]) }};]
+ button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 1 }};4,0.75;nobg;{{ worlds[2].name }}]
+
+ {% elseif len(worlds) == 3 %}
+
+ image[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 - 4.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 - 1.5 }},{{ HEIGHT * 0.5 - 4.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
+ button[{{ WIDTH * 0.5 - 2 }},{{ HEIGHT * 0.5 - 1.5 }};4,0.75;nobg;{{ worlds[1].name }}]
+
+ image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[2]) }};]
+ button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[2].name }}]
+
+ image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[3]) }};]
+ button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[3].name }}]
+
+ {% elseif len(worlds) == 4 %}
+
+ image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 - 4.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 - 4.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[1]) }};]
+ button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 - 1.5 }};4,0.75;nobg;{{ worlds[1].name }}]
+
+ image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 - 4.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 - 4.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[2]) }};]
+ button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 - 1.5 }};4,0.75;nobg;{{ worlds[2].name }}]
+
+ image[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 - 4 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[3]) }};]
+ button[{{ WIDTH * 0.5 - 4.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[3].name }}]
+
+ image[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 0.5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 + 1 }},{{ HEIGHT * 0.5 + 0.75 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', worlds[4]) }};]
+ button[{{ WIDTH * 0.5 + 0.5 }},{{ HEIGHT * 0.5 + 3.5 }};4,0.75;nobg;{{ worlds[4].name }}]
+
+ {% else %}
+ scroll_container[0,0;{{ WIDTH }},{{ HEIGHT }};worldscroll;vertical;;1,0]
+ {% set cols = floor((WIDTH - 2) / 5) %}
+ {% set trailing_cols = len(worlds) % cols %}
+ {% set rows = ceil(len(worlds) / cols) %}
+ {% set col = 0 %}
+ {% set row = 0 %}
+ {% set rx = cols * 2.5 %}
+ {% set ry = min(rows * 2.5, (HEIGHT - 2) / 2) %}
+ {% for world in worlds %}
+
+ image[{{ WIDTH * 0.5 - rx + col * 5 }},{{ HEIGHT * 0.5 - ry + row * 5 }};4,4;{{ DEFAULT_ASSETS }}btn_bg.png;]
+ image_button[{{ WIDTH * 0.5 - rx + col * 5 + 0.5 }},{{ HEIGHT * 0.5 - ry + row * 5 + 0.25 }};3,3;{{ DEFAULT_ASSETS }}menu_content.png;{{ action('set', 'selected_world', world) }};]
+ button[{{ WIDTH * 0.5 - rx + col * 5 }},{{ HEIGHT * 0.5 - ry + row * 5 + 3 }};4,0.75;nobg;{{ fe(world.name) }}]
+
+ {% set col = col + 1 %}
+ {% if col > cols - 1 %}
+ {% set col = 0 %}
+ {% set row = row + 1 %}
+ {% if row == rows - 1 and trailing_cols > 0 %}
+ {% set rx = min(trailing_cols, cols) * 2.5 %}
+ {% endif %}
+ {% endif %}
+
+ {% endfor %}
+ scroll_container_end[]
+ scrollbar[-800,0;0,0;vertical;worldscroll;]
+ {% endif %}
+
+ {% if selected_world %}
+ box[0,0;{{ WIDTH }},{{ HEIGHT }};#0003]
+ image[{{ WIDTH * 0.05 }},{{ HEIGHT * 0.05 }};{{ WIDTH * 0.9 }},{{ HEIGHT * 0.9 }};{{ DEFAULT_ASSETS }}btn_bg.png;8,8]
+ label[1,1;Selected world: {{ fe(selected_world.name) }}]
+ button[1,1;3,1;{{ action('set', 'selected_world', nil) }};Back]
+ {% endif %}
+
+ {#
+
+ 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:fi
- 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 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:fii
- @if:@damage_enabled:fiii
- image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_filled.png;.set_damage_enabled_to_0;]
- @else:fiii
- image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_empty.png;.set_damage_enabled_to_1;]
- @endif:fiii
- label[0.5,${@j + 0.25};Damage]
- @set:j:@j + 0.5
- @else:fii
-
- @endif:fii
+ {% 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:fii
- @if:@creative_enabled:fiii
- image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_filled.png;.set_creative_enabled_to_0;]
- @else:fiii
- image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_empty.png;.set_creative_enabled_to_1;]
- @endif:fiii
- label[0.5,${@j + 0.25};Creative]
- @set:j:@j + 0.5
- @else:fii
-
- @endif:fii
+ {% 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
+ {% set play_str = "Play" %}
- @if:@setting_server:fii
- @if:@server_enabled:fiii
- image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_filled.png;.set_server_enabled_to_0;]
- @set:play_str:Host server
- @else:fiii
- image_button[0,${@j};0.5,0.5;$DEFAULT_ASSET_PATH/checkbox_empty.png;.set_server_enabled_to_1;]
- @set:play_str:Play
- @endif:fiii
+ {% 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
- @else:fii
-
- @endif:fii
+ label[0.5,{{ j + 0.25 }};Server]
+ {% set j = j + 0.5 %}
+ {% endif %}
- @set:j:@j + 1
+ {% set j = j + 1 %}
- button[${@WIDTH * 0.05},${@j};${@WIDTH * 0.4},0.75;.overlay_dialog_modconfig;Configure mods]
- button[${@WIDTH * 0.05},${@j + 1};${@WIDTH * 0.4},0.75;.play;${@play_str}]
+ 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;]
- @else:fi
-
- @endif:fi
+ {% endif %}
+ #}
+
scrollbaroptions[arrows=hide]
scrollbar[-800,6;0,2;vertical;worldscroll;]
-
-
- image[${@WIDTH * 0.05 +-0.1},${@HEIGHT * 0.05 +-0.1};${@WIDTH * 0.4 + 0.2},${@HEIGHT * 0.9 +-0.75};$DEFAULT_ASSET_PATH/btn_bg.png;8,8]
- hypertext[${@WIDTH * 0.05},${@HEIGHT * 0.05};${@WIDTH * 0.4},0.75;;Available mods]
- scroll_container[${@WIDTH * 0.05},${@HEIGHT * 0.05 + 0.75};${@WIDTH * 0.4},${@HEIGHT * 0.9 +-1.7};modsscroll;vertical;;0,0]
- @foreach:@MODS:m
- @if:@i % 2:sel
- style[.select_mod_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
- style[.select_mod_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
- style[.select_mod_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
- @else:sel
- style[.select_mod_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
- style[.select_mod_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
- style[.select_mod_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
- @endif:sel
- button[0,${@i * 0.5 +-0.5};${@WIDTH * 0.4},0.5;.select_mod_${@name};${@name}]
- @endforeach:m
+
+ {% 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;;Available mods]
+ 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_ASSET_PATH/btn_bg.png;8,8]
- hypertext[${@WIDTH * 0.55},${@HEIGHT * 0.05};${@WIDTH * 0.4},0.75;;Mods for ${@selected_world_name}]
- scroll_container[${@WIDTH * 0.55},${@HEIGHT * 0.05 + 0.75};${@WIDTH * 0.4},${@HEIGHT * 0.9 +-1.7};worldmodsscroll;vertical;;0,0]
- @foreach:@WORLDMODS:wm
- @if:@game_mod:gm
- @if:@i % 2:sel
- style[.select_mod_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
- style[.select_mod_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
- style[.select_mod_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#373530]
- @else:sel
- style[.select_mod_${@name};bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
- style[.select_mod_${@name}:hovered;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
- style[.select_mod_${@name}:hovered+focused;bgimg=$DEFAULT_ASSET_PATH/white.png;bgimg_middle=0;bgcolor=#403e39]
- @endif:sel
- button[0,${@i * 0.5 +-0.5};${@WIDTH * 0.4},0.5;.select_mod_${@name};${@name}]
- @else:gm
-
- @endif:gm
- @endforeach:wm
+ 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;;Enabled mods]
+ 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_ASSET_PATH/arrow_right.png;.add_mod_to_world;]
- image_button[${@WIDTH * 0.45},${@HEIGHT * 0.5};${@WIDTH * 0.1},${@WIDTH * 0.1};$DEFAULT_ASSET_PATH/arrow_left.png;.remove_mod_from_world;]
+ 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_dialog;Cancel]
- button[${@WIDTH * 0.5},${@HEIGHT * 0.95 +-0.75};${@WIDTH * 0.4},0.75;.unoverlay_dialog;Confirm]
-
-
- label[2,2;Add World, asset path is $ASSET_PATH]
- button[2,3;3,1;.show_dialog_main;back]
+ 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]
- @foreach:[hello,there,bob]:myloop
- label[6,${@i + 3};${@item}]
- @endforeach:myloop
-
+ {% endview %}
+ {% view addworld %}
+
+ {% endview %}
]]
@@ -342,6 +400,9 @@ function get_worlds_for_game(id)
out[#out +1] = x
end
end
+ table.sort(out, function(a, b)
+ return a.name < b.name
+ end)
return out
end
@@ -996,9 +1057,16 @@ function show_game_menu(args)
end
end
- game.menu = build_game_menu(file)
+ game.menu = template.build(file)
- state.menu_vars = {}
+ state.menu_vars = {
+ get_worlds = function() return get_worlds_for_game(game.id) end,
+ get_mods = function() return get_mods_for_game(game.id) end,
+ get_world_mods = function(world)
+ world = world or state.menu_vars.selected_world
+ return world and get_mods_for_world(state.menu_vars.selected_world) or {}
+ end
+ }
state.menu_vars.setting_damage = game.disabled_settings.damage == nil
state.menu_vars.setting_creative = game.disabled_settings.creative == nil
@@ -1023,17 +1091,15 @@ function show_game_menu(args)
bgcolor[#000;true;#151618]\
"
end
-
-
+
for i, x in ipairs(state.menu_current) do
if i > 1 then
fs = fs.."\
box[0,0;"..size.x..","..size.y..";#0009]\
"
end
- fs = fs..evaluate_game_dialog(game.menu[x], state.menu_vars)
+ fs = fs..template.evaluate(game.menu[x], state.menu_vars)
end
--- print(fs)
minetest.update_formspec(game_header..fs.."\
button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\
@@ -1872,13 +1938,13 @@ function minetest.button_handler(data)
singleplayer = true
}
minetest.start()
- elseif k:sub(1, string.len(".show_dialog_")) == ".show_dialog_" then
+ elseif k:sub(1, string.len(".show_view_")) == ".show_view_" then
show_game_menu {
- show_dialog = k:sub(string.len(".show_dialog_>"))
+ show_dialog = k:sub(string.len(".show_view_>"))
}
- elseif k:sub(1, string.len(".overlay_dialog_")) == ".overlay_dialog_" then
+ elseif k:sub(1, string.len(".overlay_view_")) == ".overlay_view_" then
show_game_menu {
- overlay_dialog = k:sub(string.len(".overlay_dialog_>"))
+ overlay_dialog = k:sub(string.len(".overlay_view_>"))
}
elseif k:sub(1, string.len(".select_world_")) == ".select_world_" then
state.menu_vars.selected_world = k:sub(string.len(".select_world_>"))
@@ -1906,8 +1972,8 @@ function minetest.button_handler(data)
unoverlay_dialog = true
}
elseif k:sub(1, string.len(".set_")) == ".set_" then
- local name, value = k:match "%.set_(.+)_to_(.*)"
- state.menu_vars[name] = value == "" and "0" or value
+ local name, value = k:match "%.set_|(.+)|_to_(.*)"
+ state.menu_vars[name] = minetest.parse_json(value:gsub("\\([];,$\\[])", "%1"))
show_game_menu()
end
end
diff --git a/templates.lua b/templates.lua
index 1d3d933..2fdbbc8 100644
--- a/templates.lua
+++ b/templates.lua
@@ -124,337 +124,593 @@ the path to the builtin assets folder, and $NO_IMAGE is the path to blank.png, i
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
-local function build_template_dialog(fs, depth)
- if fs:trim() == "" then return end
- local dialog = {}
- local i = 0
- -- "(.-\n?)%s-@foreach:([^:]+):(%l*)\n(.-)\n%s-@endforeach:%3(\n?.*)"
- -- Extract foreach loops
- local prev = 0
- while i < 1000 do
- local fe_start, fe_end, fe_pattern, fe_name, fe_content = fs:find("@foreach:([^:]+):(%w-)\n(.-)\n%s-@endforeach:%2", prev)
- local if_start, if_end, if_expr, if_name, if_content, else_content = fs:find("@if:([^:]+):(%w-)\n(.-)\n%s-@else:%2\n(.-)\n%s-@endif:%2", prev)
--- print(string.rep("-", 20)..(depth or 0))
- if not fe_start and not if_start then break end
- if fe_start and fe_start < (if_start or math.huge) then
--- print("for each "..pattern.." ("..name..")\n"..content.."\nend for each ("..name..")")
- dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1, fe_start -1), (depth or 0) +1)
- dialog[#dialog +1] = {
- foreach = fe_pattern:trim(),
- name = fe_name,
- content = build_template_dialog(fe_content:trim(), (depth or 0) +1)
- }
- prev = fe_end
- i = i +1
- end
-
- if if_start and if_start < (fe_start or math.huge) then
--- print("if "..expr.." ("..name..")\n"..content.."\nend if ("..name..")")
- dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1, if_start -1), (depth or 0) +1)
- dialog[#dialog +1] = {
- condition = if_expr:trim(),
- name = if_name,
- content = build_template_dialog(if_content:trim(), (depth or 0) +1),
- else_content = build_template_dialog(else_content:trim(), (depth or 0) +1)
- }
- prev = if_end
- i = i +1
- end
- end
-
- if prev > 1 then
- dialog[#dialog +1] = build_template_dialog(fs:sub(prev +1))
- end
-
- if #dialog == 1 then dialog = dialog[1] end
- if i == 0 then dialog = fs end
--- minetest.log(dump(dialog, " "))
- return dialog
-end
-
-function build_game_menu(input)
- -- MARK: Environment variables
- input = "\n"..input
- :gsub("%$ASSET_PATH", fe(state.current_game.path.."/menu"))
- :gsub("%$DEFAULT_ASSET_PATH", fe(assets:sub(1, #assets -1)))
- :gsub("%$NO_IMAGE", fe(default_textures.."blank.png"))
- :gsub("/%*.-%*/", "")
- local menu = {}
- for name, fs in input:gmatch("%f[^\n]<(%l+)>(.-)%f[^\n]%1>") do
- if name == "meta" then
- menu[name] = parse_conf_text(fs)
- else
- menu[name] = build_template_dialog(fs)
- end
- end
--- print(dump(menu, " "))
- return menu
-end
-
-
--- Split a template expression into a binary-tree node.
-local function split_template_expression(expr)
-
- -- These are basically the operator definitions, listed from lowest
- -- (evaluated last) to highest (evaluated first) precedence.
-
- local a, b, op
- -- Or
- if not a then
- a, b, op = expr:find("(|)")
- end
- -- And
- if not a then
- a, b, op = expr:find("(&)")
- end
- -- Greater than
- if not a then
- a, b, op = expr:find("(>)")
- end
- -- Greater than or equal to
- if not a then
- a, b, op = expr:find("(>=)")
- end
- -- Less than
- if not a then
- a, b, op = expr:find("(<)")
- end
- -- Less than or equal to
- if not a then
- a, b, op = expr:find("(<=)")
- end
- -- Equal to
- if not a then
- a, b, op = expr:find("(==)")
- end
- if not a then
- a, b, op = expr:find("(=)")
- end
-
- -- Addition
- if not a then
- a, b, op = expr:find("(+)")
- end
- -- Multiplication
- if not a then
- a, b, op = expr:find("(*)")
- end
- -- Division
- if not a then
- a, b, op = expr:find("(/)")
- end
- -- Modulo
- if not a then
- a, b, op = expr:find("(%%)")
- end
- -- Exponent
- if not a then
- a, b, op = expr:find("(%^)")
- end
-
- if not a then
- return {value = expr}
- end
-
- return {
- op = op,
- lhs = split_template_expression(expr:sub(1, a -1)),
- rhs = split_template_expression(expr:sub(b +1))
- }
-end
-
--- Reduce a template expression from a binary-tree node into a single value.
-local function reduce_template_expression(tree)
- if tree.op == "|" then
- return (reduce_template_expression(tree.lhs) ~= "0" or reduce_template_expression(tree.rhs) ~= "0") and "1" or "0"
- elseif tree.op == "&" then
- return (reduce_template_expression(tree.lhs) ~= "0" and reduce_template_expression(tree.rhs) ~= "0") and "1" or "0"
- elseif tree.op == ">" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) > (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
- elseif tree.op == ">=" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) >= (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
- elseif tree.op == "<" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) < (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
- elseif tree.op == "<=" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) <= (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
- elseif tree.op == "=" or tree.op == "==" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) == (tonumber(reduce_template_expression(tree.rhs)) or 0) and "1" or "0"
- elseif tree.op == "+" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) + (tonumber(reduce_template_expression(tree.rhs)) or 0)
- elseif tree.op == "*" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) * (tonumber(reduce_template_expression(tree.rhs)) or 0)
- elseif tree.op == "/" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) / (tonumber(reduce_template_expression(tree.rhs)) or 0)
- elseif tree.op == "%" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) % (tonumber(reduce_template_expression(tree.rhs)) or 0)
- elseif tree.op == "^" then
- return (tonumber(reduce_template_expression(tree.lhs)) or 0) ^ (tonumber(reduce_template_expression(tree.rhs)) or 0)
- else
- return tree.value
- end
-end
-
--- Evaluate an interpolation expression, with an optional table of variables.
-local function evaluate_template_expression(expr, vars, depth)
- if expr == "" then return "0" end
- if not depth then depth = 0 end
- -- This handles the case where vars is omitted, because then it ends up
- -- just setting vars to an empty table.
- if type(vars) ~= "table" then
- vars = {item = vars}
- end
-
- -- Check for operators early, because variables may contain punctuation when expanded.
- -- A class is used instead of %p because %p matches @, which is used for variables.
- local has_operators = expr:find("[|&><=+*/%%^]")
-
- -- Expand all variables so we can deal with a constexpr.
- local offset = 1
- while offset < 100000 do
- local a, b, name = expr:find("@([%a_]+)", offset)
- if not a then break end
- -- If referencing an undefined variable, default to 0 because it's safest that way.
- local result = minetest.formspec_escape(tostring(vars[name] or "0"))
- expr = expr:sub(1, a -1)..result..expr:sub(b +1)
- offset = a +#result
- end
-
- -- If there are no operators, this is a constant expression and we can just return.
- if not has_operators then return expr end
-
- -- Condense sub-expressions.
- local offset = 1
- while offset < 100000 do
- local a, b, se = expr:find("(%b())", offset)
- if not a then break end
- se = se:gsub("^%((.*)%)$", "%1")
- local result = evaluate_template_expression(se, vars, depth +1)
- expr = expr:sub(1, a -1)..result..expr:sub(b +1)
- offset = a +#result
- end
-
- -- Expression parsing
- local tree = split_template_expression(expr)
- return tostring(reduce_template_expression(tree))
-end
-
--- Do variable interpolation for the given formspec using the provided variable table.
-local function evaluate_template_block(fs, vars)
- local offset = 0
- while offset < 100000 do
- local s_start, s_end, s_name, s_expr = fs:find("@set:@?([%w_]+):([^\n]+)", offset)
- local i_start, i_end, i_expr = fs:find("%${([^}]-)}", offset)
- if not s_start and not i_start then break end
- if s_start and s_start < (i_start or math.huge) then
- -- Assignment statements
- vars[s_name] = evaluate_template_expression(s_expr, vars)
- fs = fs:sub(1, s_start -1)..fs:sub(s_end +1)
- offset = s_start
- elseif i_start then
- -- Interpolations
- local result = evaluate_template_expression(i_expr, vars)
- fs = fs:sub(1, i_start -1)..result..fs:sub(i_end +1)
- offset = i_start +#result
- end
- end
- return fs
-end
-
--- Interpret the list expression of a foreach loop, then iterate.
-local function evaluate_template_foreach(loop, vars)
- local out = ""
- local list = {}
- if loop.foreach:sub(1,1) == "[" then
- list = loop.foreach:gsub("^%[(.*)%]$", "%1"):split(",")
- elseif loop.foreach:sub(1, 1) == "@" then
- local var = loop.foreach:sub(2)
- -- Builtin variables take precedence.
- if var == "WORLDS" then
- list = get_worlds_for_game(state.current_game.id)
- elseif var == "MODS" then
- list = get_mods_for_game(state.current_game.id)
- elseif var == "WORLDMODS" then
- list = state.menu_vars.selected_world and get_mods_for_world(state.menu_vars.selected_world) or {}
- else
- list = vars[var]
- end
- end
- for i, x in ipairs(list) do
- local vars2 = {}
- if type(x) ~= "table" then
- vars2.item = x
- else
- for k, v in pairs(x) do
- vars2[k] = v
- end
- end
- vars.i = i
- out = out..evaluate_game_dialog(loop.content, setmetatable(vars2, {__index = vars, __newindex = vars})).."\n"
- end
- return out
-end
-
--- Determine which branch of an if statement should be evaluated.
-local function evaluate_template_conditional(cond, vars)
- local out = ""
- local list = {}
- local condition = evaluate_template_expression(cond.condition, vars)
- if condition ~= "0" then
- out = out..evaluate_game_dialog(cond.content, vars).."\n"
- else
- out = out..evaluate_game_dialog(cond.else_content, vars).."\n"
- end
--- print("Evaluated condition `"..cond.condition.."` as "..out)
- return out
-end
-
--- Process the syntax tree for a game dialog and output the resulting string.
-function evaluate_game_dialog(dialog, vars)
- local out = ""
- if not dialog then return out end
- if type(dialog) == "string" then
- return evaluate_template_block(dialog, vars)
- elseif dialog.condition then
- out = out..evaluate_template_conditional(dialog, vars)
- elseif dialog.foreach then
- out = out..evaluate_template_foreach(dialog, vars)
- else
- for _, c in ipairs(dialog) do
- out = out..evaluate_game_dialog(c, vars)
- end
- end
- return out
-end
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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)
- return {type = "expr", value = 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)
- -- First pass: Collect all tokens into a flat list
- local block = {}
- local item
+ 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
@@ -464,18 +720,22 @@ function template.build(menu)
local comment_start, comment_end, comment = menu:find("{#(.-)#}", pos)
-- Statements
- if stmt_start and stmt_start < (expr_start or math.huge) and stmt_start < (comment_start or math.huge) then
- block[#block +1] = menu:sub(offset, stmt_start -1)
+ 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*(.*)"),
@@ -483,35 +743,95 @@ function template.build(menu)
}
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 = template.build_expr("true"),
+ 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
- print(verb)
offset = stmt_end +1
pos = stmt_end
matched = true
+
end
-- Interpolations
@@ -527,63 +847,381 @@ function template.build(menu)
-- Comments
if comment_start and comment_start < (stmt_start or math.huge) and comment_start < (expr_start or math.huge) then
- offset = expr_end
- pos = expr_end
+ 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
- return block
+ 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)
- if expr.type == "" then
-
+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 false
+ return expr.value or false
end
end
-function template.evaluate(dialog, vars)
- local out = ""
+
+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..item
+ out[#out +1] = item
else
if item.type == "if" then
for _, cond in ipairs(item.conditions) do
- if template.evaluate_expr(cond.condition) then
- out = out..template.evaluate(cond.body)
+ 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 out
+ return table.concat(out)
end
-print(template.evaluate(template.build [[
- This is a {{blah}}.
+
+function template.test()
+-- local profile = require("jit.profile")
- {% if foo %}
- Foo
- {% elseif bar %}
- Bar
- {% else %}
- Baz
- {% endif %}
+-- 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)
- {% for x in [a,b,c] %}
- The item is {{ x }}!
- Uppercase: {{ x:upper() }}
- {% endfor %}
-]], {
- foo = false
-}))
+ 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;;Available mods]
+ 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;;Enabled mods]
+ 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))