From bf1cfff244ff6fce92155599a464656d0cb487a5 Mon Sep 17 00:00:00 2001 From: Signal Date: Thu, 9 Oct 2025 12:46:56 -0400 Subject: [PATCH] Overhaul template engine and rework default game menu --- init.lua | 328 +++++++----- templates.lua | 1362 ++++++++++++++++++++++++++++++++++++------------- 2 files changed, 1197 insertions(+), 493 deletions(-) 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]") 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))