From 200e3f76429805b274060678492d4cd70398bac7 Mon Sep 17 00:00:00 2001 From: Signal Date: Thu, 2 Oct 2025 17:46:57 -0400 Subject: [PATCH] Initial commit --- arrow_left.png | Bin 0 -> 177 bytes arrow_right.png | Bin 0 -> 182 bytes bg128.png | Bin 0 -> 99 bytes bg_translucent.png | Bin 0 -> 300 bytes btn_bg.png | Bin 0 -> 288 bytes btn_bg_2.png | Bin 0 -> 267 bytes btn_bg_2_dark.png | Bin 0 -> 267 bytes btn_bg_2_dark_hover.png | Bin 0 -> 267 bytes btn_bg_2_hover.png | Bin 0 -> 267 bytes btn_bg_2_light.png | Bin 0 -> 267 bytes cancel.png | Bin 0 -> 178 bytes checkbox_empty.png | Bin 0 -> 140 bytes checkbox_filled.png | Bin 0 -> 177 bytes circle.png | Bin 0 -> 171 bytes circle_light.png | Bin 0 -> 171 bytes content.png | Bin 0 -> 180 bytes darken64.png | Bin 0 -> 99 bytes games.png | Bin 0 -> 168 bytes glow.png | Bin 0 -> 287 bytes init.lua | 1985 ++++++++++++++++++++++++++++++++++ logo_3d.png | Bin 0 -> 631 bytes main_bg.png | Bin 0 -> 101 bytes menu_content.png | Bin 0 -> 285 bytes menu_content_hovered.png | Bin 0 -> 377 bytes menu_servers.png | Bin 0 -> 313 bytes menu_servers_creative.png | Bin 0 -> 157 bytes menu_servers_favorite.png | Bin 0 -> 216 bytes menu_servers_hovered.png | Bin 0 -> 417 bytes menu_servers_icon_ping_0.png | Bin 0 -> 117 bytes menu_servers_icon_ping_1.png | Bin 0 -> 136 bytes menu_servers_icon_ping_2.png | Bin 0 -> 156 bytes menu_servers_icon_ping_3.png | Bin 0 -> 158 bytes menu_servers_icon_ping_4.png | Bin 0 -> 145 bytes menu_servers_mods.png | Bin 0 -> 246 bytes menu_servers_peaceful.png | Bin 0 -> 188 bytes menu_servers_players.png | Bin 0 -> 191 bytes menu_servers_unfavorite.png | Bin 0 -> 265 bytes menu_tab_bg.png | Bin 0 -> 265 bytes menu_tab_content.png | Bin 0 -> 273 bytes menu_tab_servers.png | Bin 0 -> 251 bytes mtmenu.png | Bin 0 -> 9059 bytes new_package.png | Bin 0 -> 121 bytes refresh.png | Bin 0 -> 183 bytes search.png | Bin 0 -> 170 bytes templates.lua | 589 ++++++++++ white.png | Bin 0 -> 95 bytes 46 files changed, 2574 insertions(+) create mode 100644 arrow_left.png create mode 100644 arrow_right.png create mode 100644 bg128.png create mode 100644 bg_translucent.png create mode 100644 btn_bg.png create mode 100644 btn_bg_2.png create mode 100644 btn_bg_2_dark.png create mode 100644 btn_bg_2_dark_hover.png create mode 100644 btn_bg_2_hover.png create mode 100644 btn_bg_2_light.png create mode 100644 cancel.png create mode 100644 checkbox_empty.png create mode 100644 checkbox_filled.png create mode 100644 circle.png create mode 100644 circle_light.png create mode 100644 content.png create mode 100644 darken64.png create mode 100644 games.png create mode 100644 glow.png create mode 100644 init.lua create mode 100644 logo_3d.png create mode 100644 main_bg.png create mode 100644 menu_content.png create mode 100644 menu_content_hovered.png create mode 100644 menu_servers.png create mode 100644 menu_servers_creative.png create mode 100644 menu_servers_favorite.png create mode 100644 menu_servers_hovered.png create mode 100644 menu_servers_icon_ping_0.png create mode 100644 menu_servers_icon_ping_1.png create mode 100644 menu_servers_icon_ping_2.png create mode 100644 menu_servers_icon_ping_3.png create mode 100644 menu_servers_icon_ping_4.png create mode 100644 menu_servers_mods.png create mode 100644 menu_servers_peaceful.png create mode 100644 menu_servers_players.png create mode 100644 menu_servers_unfavorite.png create mode 100644 menu_tab_bg.png create mode 100644 menu_tab_content.png create mode 100644 menu_tab_servers.png create mode 100644 mtmenu.png create mode 100644 new_package.png create mode 100644 refresh.png create mode 100644 search.png create mode 100644 templates.lua create mode 100644 white.png diff --git a/arrow_left.png b/arrow_left.png new file mode 100644 index 0000000000000000000000000000000000000000..567e76549d012de505776eb5367f9a802999f6c5 GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|vOQfKLo9le z6C_wI8dtSFs}JQp`@iLi;loQhip!m*xc2NYsTa7I{-ZEYx2^5A`MSj~5)Uq&+1S{~ z*slFgS^7|uFDuUho+iddb8*8f3nUm!f9_`qG2SH}C-8}Bm*NeR2c?X=6n~iXF|jZ( Y1YOb*ZcX#*Gc$ dpEVj77*=l3`uXJW2on literal 0 HcmV?d00001 diff --git a/bg128.png b/bg128.png new file mode 100644 index 0000000000000000000000000000000000000000..12d78be0d209951c090c14e91d8a5aa1106b9468 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|WIbIRLo9le tMFa#i5*9ER1TLz)(h$h(#w+0i)N+=O;eebk%W9xP22WQ%mvv4FO#tnO7P$Zb literal 0 HcmV?d00001 diff --git a/bg_translucent.png b/bg_translucent.png new file mode 100644 index 0000000000000000000000000000000000000000..6c066970fa1d95aff5943288455baa00a4508185 GIT binary patch literal 300 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|o_V@BhE&XX zds{J&$x*@~aobAAMOAHFheUTOIey^@^+>rWJ1N%r$B9q#=0tvcmYR0gK3D9=J^6pr z_Eo0Vom-xn_y2LF)_PCIEe+3#V>Dv+Ckv$#cg4gzx=h6LH|qSax;qPu4p9 z>-Ep8e@h22=q}J~5W4`9;99_H!g_&e7E=bJFVKKx3{eijWJ^EGTb7smnfY;zgRwI+ n$kdxVZ~1OM{CYXi)M94Ye=_{Kx^54EK4$QA^>bP0l+XkKUCVHq literal 0 HcmV?d00001 diff --git a/btn_bg.png b/btn_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..6bf8d0a3acbfc39f1a3160d428e634a7fbbfc0bc GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|Zh5*mhE&XX zds~t3kb?+AVAqj_7R*fr|C0q+(j6XdSfI&ix@3K$;eaT(7{rBnbKYV}umJVRhT_9PP`)s+ye$M)O zX08RSCaf2jW&zdvGHz*D#t`KY3{;`nAa+5-fjfhI=`F@4+b>+w0Gd;CXLamW>p#Ci a=FQ8HlxC2f9%l#iC4;A{pUXO@geCwFduZ7J literal 0 HcmV?d00001 diff --git a/btn_bg_2.png b/btn_bg_2.png new file mode 100644 index 0000000000000000000000000000000000000000..c32516f988ae9b203a2d970d03e83df9db3232d8 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|j(WN{hE&XX zdwU_TlcNAbAYZ3*Aam1&dPx>XUJaRV$q%_62>$E29Xk2s+&dd*TFg6lde3k5yz8ec z=5P0VYOc{BcA@9zdHHYF>7QBO^*=wp=a+Z@gYE)Npty(wcLvu2Ruk3>OtY9W7=0PH qG%RC?atNlH^szlJ$|_Pp*2FF5=4I-V+R_8`5QC?ypUXO@geCxod0UzQ literal 0 HcmV?d00001 diff --git a/btn_bg_2_dark.png b/btn_bg_2_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..c0eb271a671274e221a7c62a4a5f3d6dfed8c298 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|j(WN{hE&XX zdwU_TlcNAbAYbd@3oK0+>Lpnmg(YO(B|qePAo#E6cIf1jbMI`NX)*8I={>*I^RAz+ zn7`fcskugj*oB^-=jFdyr+;RB*Z=(Zo?qet47v+6f#M<#+!XSra$sH80a_byrEChZsCv{an^LB{Ts5M4Mch literal 0 HcmV?d00001 diff --git a/btn_bg_2_dark_hover.png b/btn_bg_2_dark_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..7f112bb5642af217ef2d89f29edf7f5a2c1d31ed GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|j(WN{hE&XX zdwU_TlcNAbU>{5K0&bxf`z1VHFf}^X=)^GE9GYLaJIiS2WNS0wO*7pFdIm`X-G|%VWx^K!r4>5SU`njxgN@xNArBGd1 literal 0 HcmV?d00001 diff --git a/btn_bg_2_hover.png b/btn_bg_2_hover.png new file mode 100644 index 0000000000000000000000000000000000000000..f523ca0b50cd23b46b58c0f9175576b7f11c9a6c GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|j(WN{hE&XX zdwU_TlcNAbAYZ4$1!m4K@jNPD8XOYr#nv&%Jv_f7)^z5bld`irAHUplW_SIgvb{au zD_@7yH8W)}o}KakPvZV!v-k)9D(cRxu4iPuz%&ae?#sBPVHrb|LokEx0?h`o3nC8O r8C(lkO{gZFcBVV8J=_gw%`?_t+B~1F7ntY)J;dPY>gTe~DWM4foE%(v literal 0 HcmV?d00001 diff --git a/btn_bg_2_light.png b/btn_bg_2_light.png new file mode 100644 index 0000000000000000000000000000000000000000..e1e9f39e218f155ec9639adc7d683e52f74414a2 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|j(WN{hE&XX zds|V+$xz@xK-VF~9n4ZE|B4Eol@+kKbKHslK=O~y+o3akw99kU`j&q-djGHU_E)2O z&({Y3|JET#-bU&bvB%NU{@f*Eud oXf}vlpq{j${w~=#Nsu+-C!h0juRhLM0`w4rr>mdKI;Vst04wQR{r~^~ literal 0 HcmV?d00001 diff --git a/cancel.png b/cancel.png new file mode 100644 index 0000000000000000000000000000000000000000..58e95ff4e5fdd27580467998997d1a7dffcc642e GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|ay(reLo9le z6BdXCT)psT{i=mO=ZBQ`7&xZ5ZqJWNKeALZR#w=MtFZH6Y=bxB8Py+80_J%6NEj@r z6g<4Xg~yR!ei0M*^fWdgSkvgiwP2mN2A50MBEk7gPGSkXE;$Q(wJR6jaPUi8B*DNC Xd)<9UtbnHj&|U^lS3j3^P6kfZCC~0t3s7R~Abbmz&`%wY46i=sF8&@VCS~kfZCC~0t3s7R~Abbmz&`%wY46i=sF8&@VCS}D|h*5J0M z8N+P1J!}fM+tx_V7XP@bVM5v*wguY9#CKT4GF*1iNc&LLI3vT=kRd+fagcLikX!`= ZL&Z@ovF{pu0YGaRJYD@<);T3K0RZDrK0yEg literal 0 HcmV?d00001 diff --git a/circle.png b/circle.png new file mode 100644 index 0000000000000000000000000000000000000000..86373f5da1348cc4efb03efe21b1f4dd7b69ff8e GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}sh%#5ArY;~ z2@nOlGBHHHSCQENf+-JZ OA%mx@pUXO@geCx_UN!Ro literal 0 HcmV?d00001 diff --git a/circle_light.png b/circle_light.png new file mode 100644 index 0000000000000000000000000000000000000000..bae8abb7c263df55d6e1804e63d552ec7664888c GIT binary patch literal 171 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}sh%#5ArY;~ z2@!- QEzm*+Pgg&ebxsLQ0D6x&ssI20 literal 0 HcmV?d00001 diff --git a/content.png b/content.png new file mode 100644 index 0000000000000000000000000000000000000000..b94ddbda5025cd8f5390e6b85b84fee676fdc5f3 GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|@;qG}Lo9le z6C^%02*x^uI)Apez`$nn&y6kOFhiO(&x2yta}f&1%Gk10mH;h( XsIAeqaHn@F&}IftS3j3^P6mHiWs z!?lj{0&Ca~n0Rz^8XP#pGF{Okwn1jb4<@nIj5kak7_~f#SYV;Z%gn&=^P7Af<1$%8 PpluAEu6{1-oD!M~E5vl?nVG3!#wP2e zb~A*Oa$UOXkU4M9S#3^1PkyFZ;b(W7b~AW}K2+0tcJ_&Xj)FLclYn}$!B1fZwH=%( zeapT(d3=q(eI%2){cpyy{Kh>^bzie<8=B^8F)%5B(F5jyeHS&%e4C#-T*#ls&Z81; zx?8}>MgPkOmIG&kCcViEW^vG%b139a%jBLcv6RV&Q+O_)6F0lq`}FYXjPC}971t9l hF-BgRds*iT+t10dLF->$i~@R+!PC{xWt~$(69BlnbH@Mx literal 0 HcmV?d00001 diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..f1c46ba --- /dev/null +++ b/init.lua @@ -0,0 +1,1985 @@ +gamedata = { + +} + +local http = minetest.get_http_api() + +-- Used for brevity when making fullscreen things +window = minetest.get_window_info() +size = window.max_formspec_size +dpi = { + x = window.size.x /size.x, + y = window.size.y /size.y +} + +-- Default textures +default_textures = minetest.get_texturepath_share().."/base/pack/" + +-- FIXME: Assets +assets = os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/" + +local version = minetest.get_version() + +-- This is where all view-specific state information goes. +state = {} + +local fe = minetest.formspec_escape +local hte = minetest.hypertext_escape + +function include(file) + -- FIXME: Use get_builtin_path() + dofile(os.getenv("HOME").."/eclipse-workspace/mods/mtmenu/"..file) +end + +dofile(minetest.get_builtin_path().."common/settings/settingtypes.lua") + + +-- Theme + +theme = { + text_color = "#aaa", + muted_text_color = "#777", + code_color = "#da8", + link_color = "#8ad", + mod_label_color = "#888", + game_label_color = "#888", + texture_pack_label_color = "#888", + + bg = "", + + modal_bg = "#0008", + + syntax_keyword = "#d8d", + syntax_keyword_value = "#79e", + syntax_identifier = "#adf", + syntax_operator = "#f66", + syntax_string = "#da8", + syntax_number = "#cdb", + syntax_comment = "#777", + syntax_placeholder = "#ccc", +} + +--End theme + + +---[[ + +minetest.set_formspec_prepend("\ + style[*;textcolor=#aaa]\ + style_type[field;border=false]\ + style_type[pwdfield;border=false]\ + 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]\ +") + +local meta_header = "formspec_version[8]\ + size["..size.x..","..size.y.."]\ + padding[0,0]\ + bgcolor[#000;true;#151618]\ + style_type[box;bordercolors=#124722;borderwidths=-5;colors=#151618]\ + image[0,"..(size.y *0.08)..");"..size.x..","..(size.x *(72/672))..";"..default_textures.."menu_header.png]\ + " +-- box[0,0;"..size.x..","..size.y..";]\ + +local game_header = "formspec_version[8]\ + size["..size.x..","..size.y.."]\ + padding[0,0]\ + bgcolor[#0000;true;#0000]\ + " +local servers_header = "formspec_version[8]\ + size["..size.x..","..size.y.."]\ + padding[0,0]\ + bgcolor[#0000;true;#151618]\ + " + +local content_header = "formspec_version[8]\ + size["..size.x..","..size.y.."]\ + padding[0,0]\ + bgcolor[#0000;true;#151618]\ + " + +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 + + label[0,0;{{ selected_world }}] + + 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 + 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:@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_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 + + @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 + + label[0.5,${@j + 0.25};Server] + @set:j:@j + 0.5 + @else:fii + + @endif:fii + + @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}] + + scroll_container_end[] + scrollbar[-800,6;0,2;vertical;worldconfigscroll;] + @else:fi + + @endif:fi + 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 + 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 + 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;] + 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] + + @foreach:[hello,there,bob]:myloop + label[6,${@i + 3};${@item}] + @endforeach:myloop + +]] + + +function core.on_before_close() + --minetest.settings:write() +end + + +minetest.async_jobs = {} + +local function handle_job(jobid, serialized_retval) + local retval = minetest.deserialize(serialized_retval) + assert(type(minetest.async_jobs[jobid]) == "function") + minetest.async_jobs[jobid](retval) + minetest.async_jobs[jobid] = nil +end + +minetest.async_event_handler = handle_job + +function minetest.handle_async(func, parameter, callback) + -- Serialize parameters + local serialized_param = minetest.serialize(parameter) + + if serialized_param == nil then + return false + end + + local jobid = minetest.do_async_callback(func, serialized_param) + + minetest.async_jobs[jobid] = callback + + return true +end + + +-- MARK: - Helpers + +local function readable_content_type(type) + if type == "game" then + return "Game" + elseif type == "mod" then + return "Mod" + elseif type == "modpack" then + return "Mod collection" + elseif type == "txp" then + return "Texture pack" + else + return "Unknown" + end +end + +-- Returns the contents of the file, or nil in case of failure. +local function read_file(path) + local f = io.open(path) + if f then + local data = f:read("a") + f:close() + if data == "" then return end + return data + end +end + +-- Check if a file exists. +local function file_exists(path) + local f = io.open(path) + if f then + f:close() + return true + end + return false +end + +-- Returns the get_games() entry corresponding to a given game ID. +local function get_game_info(game) + for _, x in ipairs(minetest.get_games()) do + if x.id == game then + return x + end + end +end + +local function get_world_index(path) + for i, x in ipairs(minetest.get_worlds()) do + if x.path == path then + return i + end + end + return -1 +end + +function get_worlds_for_game(id) + local out = {} + for _, x in ipairs(minetest.get_worlds()) do + if x.gameid == id then + out[#out +1] = x + end + end + return out +end + +function get_mods_for_game(id) + local out = {} + for _, x in ipairs(get_all_mods()) do + local conf = Settings(x.path.."mod.conf") + local unsupported_games = conf:get("unsupported_games") + if unsupported_games then + if table.indexof(unsupported_games:trim():split(","), id) == -1 then + out[#out +1] = x + end + else + local supported_games = conf:get("supported_games") + if supported_games then + if table.indexof(supported_games:trim():split(","), id) ~= -1 then + out[#out +1] = x + end + else + out[#out +1] = x + end + end + end + return out +end + +local function get_game_mods(id) + local out = {} + local base = get_game_info(id).path.."/mods/" + for _, x in pairs(minetest.get_dir_list(base, true)) do + out[#out +1] = { + name = x, + path = base..x + } + end + return out +end + +function get_mods_for_world(world) + local conf = Settings(world.."/world.mt") + local mods = {} + local game + local game_mods = {} + for k, v in pairs(conf:to_table()) do + if k == "gameid" then + game = v + elseif k:sub(1, 9) == "load_mod_" then + if v ~= "false" and v ~= "nil" and v then + mods[k:sub(10)] = minetest.get_user_path().."/"..v + end + end + end + if game then + for _, x in ipairs(get_game_mods(game)) do + game_mods[x.name] = x.path + end + end + local config = minetest.check_mod_configuration(world, mods) +-- print(dump(config)) + for _, x in ipairs(config.unsatisfied_mods) do + x.unsatisfied = true + config.satisfied_mods[#config.satisfied_mods +1] = x + end + if game then + for _, x in ipairs(config.satisfied_mods) do + if game_mods[x.name] then + x.game_provided = true + end + end + end + return config.satisfied_mods +end + +-- Returns a list of content available for download. +function get_available_content() + local version = minetest.get_version() + local cdb = minetest.settings:get("contentdb_url") + local url = cdb.."/api/packages/?type=mod&type=game&type=txp&protocol_version="..minetest.get_max_supp_proto().."&engine_version="..minetest.urlencode(version.string) + + for _, x in pairs(minetest.settings:get("contentdb_flag_blacklist"):split(",")) do + x = x:trim() + if x ~= "" then + url = url.."&hide="..minetest.urlencode(x) + end + end + + local languages + local lang = minetest.get_language() + if lang ~= "" then + languages = { lang, "en;q=0.8" } + else + languages = { "en" } + end + + local http = minetest.get_http_api() + local response = http.fetch_sync({ + url = url, + extra_headers = { + "Accept-Language: "..table.concat(languages, ", ") + }, + }) + if not response.succeeded then + return + end + local items = minetest.parse_json(response.data) + local out = {} + for _, x in pairs(items) do + out[#out +1] = x + end + return out +end + +-- Returns a list of all the installed mods. +function get_all_mods(dir) + local all = false + if not dir then + if state.all_mods then return state.all_mods end + dir = minetest.get_modpath() + all = true + end + local out = {} + for _, x in ipairs(minetest.get_dir_list(dir, true)) do + local info = minetest.get_content_info(dir.."/"..x) + if info.type == "mod" or info.type == "modpack" then + out[#out +1] = info + end + end + if all then + state.all_mods = out + end + return out +end + +-- Returns a list of all the installed games. +function get_all_games(dir) + local all = false + if not dir then + if state.all_games then return state.all_games end + dir = minetest.get_gamepath() + all = true + end + local out = {} + for _, x in ipairs(minetest.get_dir_list(dir, true)) do + local info = minetest.get_content_info(dir.."/"..x) + if info.type == "game" then + out[#out +1] = info + end + end + if all then + state.all_games = out + end + return out +end + +-- Returns a list of all the installed texture packs. +function get_all_texture_packs(dir) + local all = false + if not dir then + if state.all_texture_packs then return state.all_texture_packs end + dir = minetest.get_texturepath() + all = true + end + local out = {} + for _, x in ipairs(minetest.get_dir_list(dir, true)) do + local info = minetest.get_content_info(dir.."/"..x) + if info.type == "txp" then + out[#out +1] = info + end + end + if all then + state.all_texture_packs = out + end + return out +end + +-- Return a list of all content, as a conglomeration of the lists of mods, games, and texture packs. +function get_all_content() + if not state.all_content then + state.all_content = {} + table.insert_all(state.all_content, get_all_mods()) + table.insert_all(state.all_content, get_all_games()) + table.insert_all(state.all_content, get_all_texture_packs()) + table.sort(state.all_content, function(a, b) + if not a then + return b ~= nil + elseif not b then + return a == nil + end + return a.name < b.name + end) + end + return state.all_content +end + +-- Parses text as .conf format. +function parse_conf_text(txt) + local out = {} + local multiline = false + local key = "" + local value = "" + for line in (txt.."\n"):gmatch("(.*)\n") do + if multiline then + if line:find("\"\"\"") then + line = line:gsub("\"\"\"", ""):trim() + multiline = false + out[key] = value.."\n"..line + key = "" + value = "" + else + value = value.."\n"..line:trim() + end + else + local k, v = line:match("(.-)=(.*)") + if not k then break end + if v:find("\"\"\"") then + key = k:trim() + value = v:gsub("\"\"\"", ""):trim() + multiline = true + else + out[k:trim()] = v:trim() + end + end + end + return out +end + +local function markdown_to_hypertext_format(md) + return md:gsub("\n%s*####%s*([^\n]-)\n", "\n%1\n") + :gsub("\n%s*###%s*([^\n]-)\n", "\n%1\n") + :gsub("([^\n]-)\n%-+\n", "\n%1\n") + :gsub("\n%s*##%s*([^\n]-)\n", "\n%1\n") + :gsub("\n%s*#%s*([^\n]-)\n", "\n\n") + :gsub("%*%*([^`\n]-)%*%*", "%1") + :gsub("%*([^`\n]-)%*", "%1") + :gsub("__([^`\n]-)__", "%1") + :gsub("\n_([^`\n]-)_", "%1") + :gsub("%*%s+(.-)\n", "• %1\n") +-- :gsub("```%a-\n(.-)```", "") +-- :gsub("`([^`]-)`", "") + -- Since we can't (and shouldn't) display images over HTTPS at all, simply get rid of them. + :gsub("%[?!%[[^%]]-%]%([^\n]+%)", "") +end + + +local keywords = { + "and", "break", "do", "else", "elseif", "end", --[["false",]] "for", "function", + "goto", "if", "in", "local", --[["nil",]] "not", "or", "repeat", "return", + "then", --[["true",]] "until", "while" +} +local keyword_set = {} +for _, kw in ipairs(keywords) do + keyword_set[kw] = true +end + +local keyword_value_set = { + ["true"] = true, + ["false"] = true, + ["nil"] = true, + ["math.huge"] = true +} + +local function colorize(str, color) + return "" +end + +local function syntax_highlight(code) + local out = "" + local pos = 1 + while pos <= #code do + local matched = false + + -- Whitespace + local ws_start, ws_end = code:find("^%s+", pos) + if ws_start then + out = out..colorize(code:sub(ws_start, ws_end), "whitespace") + pos = ws_end + 1 + matched = true + end + + -- Single-line comment + if not matched then + local com_start, com_end = code:find("^%-%-[^\n]*", pos) + if com_start then + out = out..colorize(code:sub(com_start, com_end), "comment") + pos = com_end + 1 + matched = true + end + end + + -- String (double-quoted, simple escape handling) + if not matched then + local str_start, str_end = code:find('^"[^"]*"', pos) + if str_start then + out = out..colorize(code:sub(str_start, str_end), "string") + pos = str_end + 1 + matched = true + end + end + + -- String (single-quoted, similar) + if not matched then + local str_start, str_end = code:find("^'[^']*", pos) + if str_start then + out = out..colorize(code:sub(str_start, str_end), "string") + pos = str_end + 1 + matched = true + end + end + + -- Documentation placeholders. These aren't in Lua, but that means I + -- can highlight them without worrying about confusing alternative meanings. + if not matched then + local str_start, str_end = code:find('^\\?<[^>]*>', pos) + if str_start then + out = out..colorize(code:sub(str_start, str_end), "placeholder") + pos = str_end + 1 + matched = true + end + end + + -- Number (basic integer/float) + if not matched then + local num_start, num_end = code:find("^%d+%.?%d*[eE]?[+-]?%d*", pos) + if num_start then + out = out..colorize(code:sub(num_start, num_end), "number") + pos = num_end + 1 + matched = true + end + end + + -- Identifier/Keyword + if not matched then + local id_start, id_end, id_text = code:find("^([%a_][%w_]*)", pos) + if id_start then + if keyword_set[id_text] then + out = out..colorize(id_text, "keyword") + elseif keyword_value_set[id_text] then + out = out..colorize(id_text, "keyword_value") + else + out = out..colorize(id_text, "identifier") + end + pos = id_end + 1 + matched = true + end + end + + -- Operators/Punctuation (catch-all for non-alphanumeric) + if not matched then + local op_start, op_end = code:find("^[^%s%a%d_\"'{}(),%][]", pos) + if op_start then + local op = code:sub(op_start, op_end) + out = out..colorize(op, op == "-" and code:sub(op_end +1, op_end +2):find "^%d" and "number" or "operator") + pos = op_end + 1 + matched = true + end + end + + -- If nothing matched, advance to avoid infinite loop (e.g., invalid char) + if not matched then + out = out..colorize(code:sub(pos, pos), "generic") + pos = pos + 1 + end + end + return out +end + +local function markdown_to_hypertext(md) + md = ("\n"..hte(md or "").."\n") + local out = "" + -- The following is necessary because code blocks should not have formatting applied to their contents. + local offset = 1 + while true do + local block_start, block_end, lang, block = md:find("```(%a*)\n(.-)```", offset) + local code_start, code_end, code = md:find("[^`]`([^`\n]+)`", offset) + if not block_start and not code_start then break end + if block_start and block_start < (code_start or math.huge) then + -- Don't apply syntax highlighting to blocks annotated as a language other than Lua. + -- However, assume that blocks with no annotation are probably Lua. + if lang == "lua" then + block = syntax_highlight(block) + end + -- The block:gsub hack is used so that leading whitespace (indentation) is not trimmed. + out = out..markdown_to_hypertext_format(md:sub(offset, block_start -1)).."" + offset = block_end +1 + end + if code_start and code_start < (block_start or math.huge) then + out = out..markdown_to_hypertext_format(md:sub(offset, code_start)).."" + offset = code_end +1 + end + end + out = out..markdown_to_hypertext_format(md:sub(offset)) +-- md = md +-- :gsub("\n%s*####%s*([^\n]-)\n", "\n%1\n") +-- :gsub("\n%s*###%s*([^\n]-)\n", "\n%1\n") +-- :gsub("([^\n]-)\n%-+\n", "\n%1\n") +-- :gsub("\n%s*##%s*([^\n]-)\n", "\n%1\n") +-- :gsub("\n%s*#%s*([^\n]-)\n", "\n\n") +-- :gsub("%*%*([^`\n]-)%*%*", "%1") +-- :gsub("%*([^`\n]-)%*", "%1") +-- :gsub("__([^`\n]-)__", "%1") +-- :gsub("\n_([^`\n]-)_", "%1") +-- :gsub("%*%s+(.-)\n", "• %1\n") +-- -- Since we can't (and shouldn't) display images over HTTPS at all, simply get rid of them. +-- :gsub("%[?!%[[^%]]-%]%([^\n]+%)", "") +-- print(md) + return fe(out) +end + + +include "templates.lua" + +--[[ MARK: - Stylesheet Parser + The question of custom themes being what it is, and in order to avoid an + unreasonably convoluted settings page, I've opted to allow users to declare + themes simply by editing a stylesheet. + + Stylesheets are defined in basic toml format, like so: + ``` + [button] + + color = #faa + + [label] + + color = #444 + + [.table_even] + + bgcolor = #111 + ``` + + Each section begins with a header (denoted by brackets), which specifies the type of element to apply the style to. Prefixing a header name with a dot will cause that header to match elements which are given a certain class. + + Rules take the form of ` = `. Rules specified prior to any header will apply to all elements. +--]] + +local function build_stylesheet_rules(rules) + local out = {} + local offset = 1 + while true do + local a, b, key, value = rules:find("(%w+)%s*=%s*([%w%p]+)", offset) + if not a then break end + out[key] = value + offset = b + end + return out +end + +function build_stylesheet(sheet) + -- Strip commetns + sheet = sheet:gsub("%-%-.*\n", "") + local out = {} + local last_header + local offset = 1 + while true do + local a, b, header = sheet:find("%[%s*([%a._-]+)%s*%]", offset) + if not a then break end + out[last_header or "*"] = build_stylesheet_rules(sheet:sub(offset, a -1)) + last_header = header + offset = b +1 + end + out[last_header or "*"] = build_stylesheet_rules(sheet:sub(offset)) + return out +end + +print(dump(build_stylesheet([[ + [button] + + color = #faa + + [label] + + color = #444 +]]))) + +function evaluate_stylesheet() + +end + +-- MARK: - Views + +-- The meta menu, for choosing which game menu to enter or entering the servers/content views. +local last_game = minetest.settings:get("menu_last_game") +function show_meta_menu(v) + state.loc = "games" + local games = minetest.get_games() + if #games < 1 then + minetest.update_formspec(meta_header.."\ + hypertext[0,0;"..size.x..","..size.y..";ht;\ + \ + ]\ + ") + return + end + local idx = 1 + for i, x in ipairs(games) do + if x.id == last_game then idx = i end + end + if v then idx = v else v = idx end + local fs = "" + + + fs = fs.."\ + scroll_container[0,0;"..size.x..","..size.y..";carousel;horizontal;;]\ + box[0,0;"..(#games *5)..",1;#0000]\ + " + + + local center = (size.x /2) +(v /10) + + for i, x in ipairs(games) do + fs = fs.."\ + image_button["..(center +(i -(#games /2)) *0.25)..","..(size.y -0.5)..";0.25,0.25;"..assets..(i == idx and "circle_light" or "circle")..".png;jump"..i..";]\ + " + + local dist = i -idx + local scale = 4 /(math.abs(dist) +1) + if scale > 0.1 then + --This looks neat, but is useless for UI purposes: center +10^(1 /math.abs(dist)) *math.sign(dist) + local lc = center +math.abs(dist *10)^0.55 *math.sign(dist) + if x.menuicon_path == "" then + x.menuicon_path = assets.."games.png" + end + --TODO: Do these need some kind of background? + fs = fs.."\ + image_button["..(lc -(scale /2))..","..(size.y /2 -(scale /2) +(4 -scale) /2 +2)..";"..scale..","..scale..";"..fe(x.menuicon_path)..";game"..fe(x.id)..";]\ + tooltip[game"..fe(x.id)..";"..fe(x.title)..";#444;#aaa]\ + " + if dist == 0 then + fs = fs.."\ + button["..(lc -(scale /2))..","..(size.y /2 +4)..";4,0.5;nobg;"..fe(x.title).."]\ + " + end + end + end + + + fs = fs.."\ + scroll_container_end[]\ + scrollbaroptions[min=1;max="..#games..";smallstep=1]\ + scrollbar[0,-800;"..size.x..",0;horizontal;carousel;"..(v or "").."]\ + " + + minetest.update_formspec(meta_header..fs.."\ + style[show_servers_view;border=false;bgimg="..fe(assets).."menu_servers.png;bgimg_middle=0]\ + style[show_servers_view:hovered;border=false;bgimg="..fe(assets).."menu_servers_hovered.png;bgimg_middle=0]\ + button["..(size.x *0.05)..","..(size.y *0.3)..";4,4;show_servers_view;]\ + button["..(size.x *0.05)..","..(size.y *0.3 +4)..";4,0.5;nobg;Servers]\ + style[show_content_view;border=false;bgimg="..fe(assets).."menu_content.png;bgimg_middle=0]\ + style[show_content_view:hovered;border=false;bgimg="..fe(assets).."menu_content_hovered.png;bgimg_middle=0]\ + button["..(size.x -(size.x *0.05) -4)..","..(size.y *0.3)..";4,4;show_content_view;]\ + button["..(size.x -(size.x *0.05) -4)..","..(size.y *0.3 +4)..";4,0.5;nobg;Content]\ + style[show_settings_view,show_about_view;bgimg="..assets.."menu_tab_bg.png]\ + style[show_settings_view:hovered,show_about_view:hovered;bgimg="..assets.."menu_tab_bg.png;bgcolor=#fffd]\ + button[1,0;2,0.5;show_about_view;About]\ + button["..(size.x -3)..",0;2,0.5;show_settings_view;Settings]\ + ") +end + +-- Shows games' custom menus. +function show_game_menu(args) + if not state.current_game then + minetest.log("warning", "Main menu attempted to show a menu for a nonexistent game; aborting.") + return + end + if not args then args = {} end + local game = state.current_game + if not game.menu then + local file = read_file(game.path.."/menu/mainmenu.txt") or default_game_menu + + game.disabled_settings = {} + local settings = Settings(game.path.."/game.conf") + + local disabled_settings = string.split(settings:get("disabled_settings") or "", ",") + for _, x in ipairs(disabled_settings) do + x = x:trim() + if x == "enable_damage" then + game.disabled_settings.damage = false + elseif x == "!enable_damage" then + game.disabled_settings.damage = true + elseif x == "creative_mode" then + game.disabled_settings.creative = false + elseif x == "!creative_mode" then + game.disabled_settings.creative = true + elseif x == "enable_server" then + game.disabled_settings.server = false + end + end + + local overlays = {} + local backgrounds = {} + local headers = {} + local footers = {} + local music = {} + + for _, x in ipairs(minetest.get_dir_list(game.path.."/menu", false)) do + if x:sub(1, string.len("background")) == "background" then + local a, b, id = x:find("background.(%d+).png") + if a or x == "background.png" then + backgrounds[(tonumber(id) or 0) +1] = x + end + elseif x:sub(1, string.len("overlay")) == "overlay" then + local a, b, id = x:find("overlay.(%d+).png") + if a or x == "overlay.png" then + overlays[(tonumber(id) or 0) +1] = x + end + elseif x:sub(1, string.len("header")) == "header" then + local a, b, id = x:find("header.(%d+).png") + if a or x == "header.png" then + headers[(tonumber(id) or 0) +1] = x + end + elseif x:sub(1, string.len("footer")) == "footer" then + local a, b, id = x:find("footer.(%d+).png") + if a or x == "footer.png" then + footers[(tonumber(id) or 0) +1] = x + end + elseif x:sub(1, string.len("theme")) == "theme" then + local a, b, id = x:find("theme.(%d+).ogg") + if a or x == "theme.ogg" then + music[(tonumber(id) or 0) +1] = x + end + end + end + + if #backgrounds > 0 then + local path = game.path.."/menu/"..backgrounds[math.random(#backgrounds)] + if minetest.set_background("background", path) then + game.background = path + end + elseif #overlays > 0 then + local path = game.path.."/menu/"..overlays[math.random(#overlays)] + -- I don't know why, but using "overlay" here simply does nothing. Whatever + -- the difference is, it isn't visual, so this effectively 'fixes' it. + if minetest.set_background("background", path) then + game.overlay = path + end + else + minetest.set_background("background", "") + end + + if #headers > 0 then + local path = game.path.."/menu/"..headers[math.random(#headers)] + if minetest.set_background("header", path) then + game.header = path + end + end + + if #music > 0 then + local path = game.path.."/menu/"..music[math.random(#music)] + if minetest.set_background("header", path) then + game.music = path + end + end + + game.menu = build_game_menu(file) + + state.menu_vars = {} + + state.menu_vars.setting_damage = game.disabled_settings.damage == nil + state.menu_vars.setting_creative = game.disabled_settings.creative == nil + state.menu_vars.setting_server = game.disabled_settings.server == nil + end + + if args.show_dialog then + state.menu_current = {args.show_dialog} + elseif args.overlay_dialog then + state.menu_current[#state.menu_current +1] = args.overlay_dialog + elseif args.unoverlay_dialog and #state.menu_current > 1 then + state.menu_current[#state.menu_current] = nil + end + + state.menu_vars.WIDTH = size.x + state.menu_vars.HEIGHT = size.y + + local fs = "" + + if not game.background and not game.overlay then + fs = "\ + 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) + end +-- print(fs) + + minetest.update_formspec(game_header..fs.."\ + button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\ + ") +end + +function add_favorite_server(address, port) + state.favorite_servers[#state.favorite_servers +1] = {address = address, port = port} + local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w") + f:write(minetest.write_json(state.favorite_servers)) + f:flush() + f:close() +end + +function remove_favorite_server(address, port) + local idx + for i, x in pairs(state.favorite_servers) do + if x.address == address and x.port == port then + idx = i + break + end + end + if idx then + table.remove(state.favorite_servers, idx) + local f = io.open(minetest.get_user_path().."/client/serverlist/favoriteservers.json", "w") + f:write(minetest.write_json(state.favorite_servers)) + f:flush() + f:close() + end +end + +function refresh_server_list() + minetest.handle_async( + function(param) + local http = minetest.get_http_api() + local url = ("%s/list?proto_version_min=%d&proto_version_max=%d"):format( + "https://servers.luanti.org" or minetest.settings:get("serverlist_url"), + minetest.get_min_supp_proto(), + minetest.get_max_supp_proto() + ) + + local response = http.fetch_sync{url = url} + if not response.succeeded then + return {} + end + + local retval = minetest.parse_json(response.data) + return retval and retval.list or {} + end, + nil, + function(result) + local list = table.copy(state.favorite_servers) + table.insert_all(list, result) + state.serverlist = list + if state.serverlist_filtered then + state.serverlist_filtered = search_server_list(state.servers_filter) + end + if state.loc == "servers" then + show_servers_menu() + end + end + ) +end + +function search_server_list(input) + if input:trim() == "" then + return nil + end + + local search_str = "" + local words = {} + local mods = {} + local games = {} + local players = {} + + input = input:lower() + + for x in input:gmatch("%S+") do + if x:sub(1, 4) == "mod:" then + mods[#mods +1] = x:sub(5) + elseif x:sub(1, 7) == "player:" then + players[#players +1] = x:sub(8) + elseif x:sub(1, 5) == "game:" then + games[#games +1] = x:sub(6) + else + words[#words +1] = x + end + end + +-- print(dump({words = words, mods= mods, players =players, games = games}, " ")) + + local out = {} + for _, x in ipairs(state.serverlist) do + local passed = true + + for _, a in ipairs(words) do + if not (x.description and x.description:lower():find(a, 1, true) or x.name and x.name:lower():find(a, 1, true)) then + passed = false + end + end + + -- PUC Lua doesn't have `continue`, hence the indentation. + if passed then + if #games > 0 then + passed = false + end + for _, a in ipairs(games) do + if a == x.gameid then + passed = true + end + end + + if passed then + if x.mods and #mods > 0 then + local found = 0 + for _, a in ipairs(x.mods) do + for _, b in ipairs(mods) do + if a == b then + found = found +1 + end + end + end + passed = found == #mods + else + passed = not not x.mods + end + + if passed then + if x.clients_list and #players > 0 then + local found = 0 + for _, a in ipairs(x.clients_list) do + for _, b in ipairs(players) do + if a:lower() == b then + found = found +1 + end + end + end + + passed = found == #players + else + passed = not not x.clients_list + end + if passed then + out[#out +1] = x + end + end + end + end + end + return out +end + +-- Shows the server list. +function show_servers_menu() + local fs = "" + local loading + if not state.serverlist then + loading = true + refresh_server_list() + local favorite_servers = minetest.parse_json(read_file(minetest.get_user_path().."/client/serverlist/favoriteservers.json") or "{}") + for _, x in ipairs(favorite_servers) do + x.favorite = true + end + state.favorite_servers = favorite_servers + state.serverlist = favorite_servers + end + + -- Overlay dialogs don't occlude area tooltips, so such tooltips are disabled in that case. + local showing_dialog = state.joining_server or state.connecting_to_server or state.showing_server_mods or state.showing_server_players + + local current_server + local joining_server + + fs = "\ + style[servers_filter;border=false;textcolor=#aaa]\ + image["..(size.x *0.095)..","..(size.y *0.1 -0.85)..";"..(size.x *0.81 -5.65)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\ + field["..(size.x *0.095 +0.1)..","..(size.y *0.1 -0.85)..";"..(size.x *0.81 -5.45)..",0.75;servers_filter;;"..(state.menu_vars.servers_filter or "").."]\ + style[servers_search,servers_refresh,servers_cancel;bgimg="..assets.."btn_bg_2.png;bgimg_middle=8,8]\ + image_button["..(size.x *0.91 -5.65)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."search.png;servers_search;]\ + tooltip[servers_search;Search server list;#444;#aaa]\ + image_button["..(size.x *0.91 -4.8)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."cancel.png;servers_cancel;]\ + tooltip[servers_cancel;Cancel search;#444;#aaa]\ + image_button["..(size.x *0.91 -3.95)..","..(size.y *0.1 -0.85)..";0.75,0.75;"..assets.."refresh.png;servers_refresh;]\ + tooltip[servers_refresh;Refresh server list;#444;#aaa]\ + button["..(size.x *0.91 -3.1)..","..(size.y *0.1 -0.85)..";3,0.75;direct_connection;Direct Connection...]\ + image["..(size.x *0.095)..","..(size.y *0.1)..";"..(size.x *0.81)..","..(size.y *0.8)..";"..assets.."btn_bg.png;8,8]\ + scroll_container[0,"..(size.y *0.1 +0.1)..";"..size.x..","..(size.y *0.8 -0.2)..";serverscroll;vertical;;0,0]\ + " + local listx = size.x *0.1 + local infox = state.current_server and size.x *0.6 or size.x *0.9 + local max = size.x *0.9 + local i = 0 + for idx, x in ipairs(state.serverlist_filtered or state.serverlist) do + -- Skip incompatible servers. You can't join them, so showing them is rather pointless. + if (x.proto_max or version.proto_min) >= version.proto_min then + local ping_lvl = 0 + local lag = (x.lag or 0) * 1000 + (x.ping or 0) * 250 + if lag <= 125 then + ping_lvl = 4 + elseif lag <= 175 then + ping_lvl = 3 + elseif lag <= 250 then + ping_lvl = 2 + elseif lag <= 400 then + ping_lvl = 1 + end + + local name = x.name and x.name:trim() or "" + if name == "" then + name = minetest.colorize("#888", x.address..":"..x.port) + end + local label_offset = 0.1 + fs = fs.."\ + style[serverinfo"..idx..";border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#"..(i %2 == 1 and "373530" or "403e39").."ff]\ + style[serverinfo"..idx..":hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0]\ + button["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;serverinfo"..idx..";]\ + " + if x.favorite then + fs = fs.."\ + image["..(listx)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_favorite.png]\ + "..(showing_dialog and "" or "tooltip["..(listx)..","..(i *0.5)..";0.5,0.5;Favorite server;#444;#aaa]\ + ") + label_offset = 0.6 + end + local clients = "" + local color = "#aaa" + local icons_offset = (infox -0.6) + if x.clients then + icons_offset = icons_offset -1 + clients = x.clients..(x.clients_max and "/"..x.clients_max or "") + if x.clients > 0 and x.clients_max then + local percent = x.clients /x.clients_max + if percent < 0.75 then + color = "#638b67" + elseif percent < 1 then + color = "#a69174" + else + color = "#9d5b5b" + end + end + fs = fs.."\ + hypertext["..icons_offset..","..(i *0.5)..";1,0.5;;"..fe(clients).."]\ + " + end + if x.pvp == false then + icons_offset = icons_offset -0.5 + fs = fs.."\ + image["..(icons_offset)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_peaceful.png]\ + "..(showing_dialog and "" or "tooltip["..(icons_offset)..","..(i *0.5)..";0.5,0.5;Peaceful;#444;#aaa]\ + ") + end + if x.creative then + icons_offset = icons_offset -0.5 + fs = fs.."\ + image["..(icons_offset)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_creative.png]\ + "..(showing_dialog and "" or "tooltip["..(icons_offset)..","..(i *0.5)..";0.5,0.5;Creative;#444;#aaa]\ + ") + end + fs = fs.."\ + label["..(listx +label_offset)..","..(i *0.5 +0.25)..";"..fe(name).."]\ + "..((x.ping or x.lag) and "image["..(infox -0.6)..","..(i *0.5)..";0.5,0.5;"..assets.."menu_servers_icon_ping_"..ping_lvl..".png]\ + "..(showing_dialog and "" or "tooltip["..(infox -0.6)..","..(i *0.5)..";0.5,0.5;Ping: "..math.floor(lag)..";#444;#aaa]\ + ") or "") + i = i +1 + end + end + if loading then + fs = fs.."\ + hypertext["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;;Loading...]\ + " + end + if i < 1 then + fs = fs.."\ + hypertext["..listx..","..(i *0.5)..";"..(infox -listx)..",0.5;;No servers found.]\ + " + end + fs = fs.."\ + scroll_container_end[]\ + scrollbar[-800,0;1,1;vertical;serverscroll;"..(state.menu_vars.serverscroll and tonumber(state.menu_vars.serverscroll:sub(5)) or 0).."]\ + " + -- Server detail view. + if state.current_server then + fs = fs.."box["..(infox -0.05)..","..(size.y *0.1 +0.05)..";0.1,"..(size.y *0.8 -0.1)..";#292d2fff]\ + container["..infox..","..(size.y *0.1).."]\ + hypertext[0,0;"..(max -infox)..",0.75;;"..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."]\ + image[0.1,0.75;"..(max -infox -0.2)..",0.5;"..assets.."btn_bg_2_dark.png;8,8]\ + box["..((max -infox) *0.7 -0.05)..",0.75;0.1,0.5;#292d2fff]\ + hypertext[0.2,0.8;"..((max -infox) *0.7 -0.2)..",0.5;server_address;"..fe(hte(state.current_server.address)).."]\ + hypertext["..((max -infox) *0.7 +0.1)..",0.8;"..((max -infox) *0.3 -0.2)..",0.5;server_port;"..fe(hte(tostring(state.current_server.port))).."]\ + " + local buttons_offset = 0.1 + if state.current_server.mods then + fs = fs.."\ + image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_mods.png;show_server_mods;]\ + tooltip[show_server_mods;Show mod list;#444;#aaa]\ + " + buttons_offset = buttons_offset +0.5 + end + if state.current_server.clients_list then + fs = fs.."\ + image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_players.png;show_server_players;]\ + tooltip[show_server_players;Show player list;#444;#aaa]\ + " + buttons_offset = buttons_offset +0.5 + end + if state.current_server.favorite then + fs = fs.."\ + image_button["..buttons_offset..",1.5;0.5,0.5;"..assets.."menu_servers_unfavorite.png;unfavorite_server;]\ + tooltip[unfavorite_server;Remove from favorites;#444;#aaa]\ + " + buttons_offset = buttons_offset +0.5 + end + local y = buttons_offset > 0.1 and 2.25 or 1.75 + fs = fs.."\ + hypertext[0.1,"..y..";"..(max -infox -0.2)..","..((size.y *0.8 -1.5 -1.05))..";server_desc;"..fe(hte(state.current_server.description or "")).."]\ + button[0.1,"..(size.y *0.8 -1)..";"..(max -infox -0.2)..",0.75;join_server;Join Server]\ + container_end[]\ + " + end + + fs = fs.."\ + button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\ + " + + -- Mod list dialog. + if state.showing_server_mods then + fs = fs.."\ + box[0,0;"..size.x..","..size.y..";#0008]\ + style[_even,_even:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\ + style[_odd,_odd:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#373530ff]\ + image["..(size.x /4 -0.1)..","..(size.y /4 -0.1)..";"..(size.x /2 +0.2)..","..(size.y /2 +0.2)..";"..assets.."btn_bg.png;8,8]\ + scroll_container["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2 -1)..";modsscroll;vertical;;0,0]\ + " + for i, x in ipairs(state.current_server.mods) do + fs = fs.."\ + button[0,"..(i *0.5 -0.5)..";"..(infox -listx)..",0.5;"..(i %2 == 1 and "_odd" or "_even")..";]\ + label[0.1,"..(i *0.5 -0.25)..";"..fe(x).."]\ + " + end + fs = fs.."\ + scroll_container_end[]\ + scrollbar[-800,0;1,1;vertical;modsscroll;]\ + button["..(size.x /4)..","..(size.y *0.75 -0.875)..";"..(size.x /2)..",0.75;close_dialog;Back]\ + " + end + + -- Player list dialog. + if state.showing_server_players then + fs = fs.."\ + box[0,0;"..size.x..","..size.y..";#0008]\ + style[_even,_even:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\ + style[_odd,_odd:hovered;border=false;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#373530ff]\ + image["..(size.x /4 -0.1)..","..(size.y /4 -0.1)..";"..(size.x /2 +0.2)..","..(size.y /2 +0.2)..";"..assets.."btn_bg.png;8,8]\ + scroll_container["..(size.x /4)..","..(size.y /4)..";"..(size.x /2)..","..(size.y /2 -1)..";modsscroll;vertical;;0,0]\ + " + for i, x in ipairs(state.current_server.clients_list) do + fs = fs.."\ + button[0,"..(i *0.5 -0.5)..";"..(infox -listx)..",0.5;"..(i %2 == 1 and "_odd" or "_even")..";]\ + label[0.1,"..(i *0.5 -0.25)..";"..fe(x).."]\ + " + end + fs = fs.."\ + scroll_container_end[]\ + scrollbar[-800,0;1,1;vertical;modsscroll;]\ + button["..(size.x /4)..","..(size.y *0.75 -0.875)..";"..(size.x /2)..",0.75;close_dialog;Back]\ + " + end + + -- Connection dialog. + if state.joining_server or state.connecting_to_server then + local offset = 0 + fs = fs.."\ + box[0,0;"..size.x..","..size.y..";#0008]\ + image["..(size.x *0.25 -0.2)..","..(size.y *0.25 -0.1)..";"..(size.x *0.5 +0.4)..","..(size.y *0.5 +0.2)..";"..assets.."btn_bg.png;8,8]\ + scroll_container["..(size.x *0.25)..","..(size.y *0.25)..";"..(size.x *0.5)..","..(size.y *0.5 +0.2)..";serverconfscroll;vertical;;0,0]\ + " + + if state.server_connection_error then + offset = offset +0.75 + fs = fs.."\ + hypertext[0,0;"..(size.x *0.5)..",0.75;;"..fe(hte(state.server_connection_error.msg)).."]\ + set_focus["..fe(state.server_connection_error.element)..";true]\ + " + else + fs = fs.."\ + set_focus[server_"..(state.joining_server and (minetest.settings:get("name") and "password" or "username") or "address")..";true]\ + " + end + + -- Make the address and port immutable if joining from the serverlist. + if state.joining_server then + fs = fs.."\ + hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;Connecting to "..fe(hte(state.current_server.name or state.current_server.address..":"..state.current_server.port)).."...]\ + " + offset = offset +1 + else + fs = fs.."\ + set_focus[server_address;true]\ + hypertext[0,"..offset..";"..(size.x *0.5)..",0.75;;Connecting to server...]\ + " + offset = offset +1 + fs = fs.."\ + image[0,"..(0.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\ + label[0.2,"..(0.1 +offset)..";Address]\ + field[0.2,"..(0.25 +offset)..";"..(size.x *0.4 -0.4)..",0.75;server_address;;"..(state.menu_vars.server_address or "").."]\ + box["..(size.x *0.4 -0.25)..","..(0.25 +offset)..";0.1,0.75;#292d2fff]\ + label["..(size.x *0.4 -0.2)..","..(0.1 +offset)..";Port]\ + field["..(size.x *0.4)..","..(0.25 +offset)..";"..(size.x *0.1 -0.2)..",0.75;server_port;;"..(state.menu_vars.server_port or "").."]\ + " + offset = offset +1 + end + fs = fs.."\ + image[0,"..(0.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\ + label[0.2,"..(0.1 +offset)..";Username]\ + field[0.2,"..(0.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_username;;"..minetest.settings:get("name").."]\ + image[0,"..(1.25 +offset)..";"..(size.x *0.5)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\ + style[server_password;border=false;textcolor=#aaa;bgimg="..assets.."btn_bg.png]\ + label[0.2,"..(1.1 +offset)..";Password]\ + pwdfield[0.2,"..(1.25 +offset)..";"..(size.x *0.5 -0.4)..",0.75;server_password;]\ + button[0,"..(2.25 +offset)..";"..(size.x *0.5)..",0.75;cancel_join_server;Cancel]\ + button[0,"..(3.25 +offset)..";"..(size.x *0.5)..",0.75;confirm_join_server;Join]\ + scroll_container_end[]\ + scrollbar[-800,0;1,1;vertical;serverconfscroll;"..(state.menu_vars.serverconfscroll and tonumber(state.menu_vars.serverconfscroll:sub(5)) or 0).."]\ + " + end + minetest.update_formspec(servers_header..fs) +end + +-- Show the content manager. +function show_content_menu() + local fs = "" + + if not state.all_content then + get_all_content() + end + + local w = size.x + local h = size.y -1.1 + + if not state.current_package then + + fs = "\ + scroll_container[0,1.1;"..w..","..h..";contentscroll;horizontal;;]\ + " + + local cols = math.floor(w /2.5) + local spacing_x = w %2.5 /cols +0.5 + + local rows = math.floor(h /3) + local spacing_y = h %3 /rows +0.5 + + local pages = math.ceil(#state.all_content /(rows *cols)) + local page = state.content_page or 1 + + local offset = page /10 + + local x = 0 + local y = 0 + local i = (page -1) *(rows *cols) +1 + + while true do + local pkg = state.all_content[i] + if not pkg then break end + if not pkg.icon then + pkg.icon = pkg.type == "game" and pkg.path.."/menu/icon.png" or pkg.path.."/icon.png" + if not file_exists(pkg.icon) then + pkg.icon = assets.."menu_content.png" + end + end + local title = pkg.title:trim() + if title == "" then + title = pkg.name + end + fs = fs.."\ + image_button["..(x *(2 +spacing_x) +(spacing_x /2) +offset)..","..(y *(2 +spacing_y) +(spacing_y /2))..";2,2;"..fe(pkg.icon)..";view_package_"..i..";]\ + tooltip[view_package_"..i..";"..fe(title)..";#444;#aaa]\ + hypertext["..(x *(2 +spacing_x) +(spacing_x /2) +offset)..","..(y *(2 +spacing_y) +(spacing_y /2) +2)..";2,0.75;nobg;"..fe(hte(title)).."]\ + " + i = i +1 + x = x +1 + if x > cols -1 then + x = 0 + y = y +1 + if y > rows -1 then break end + end + end + + fs = fs.."\ + box[0,0;"..(pages *w)..",1;#0000]\ + scroll_container_end[]\ + scrollbaroptions[min=1;max="..pages..";smallstep=1]\ + scrollbar[-800,0;1,1;horizontal;contentscroll;"..page.."]\ + " + + fs = fs.."\ + box[0,0;"..size.x..",1;#403e39ff]\ + box[0,1;"..size.x..",0.1;#292d2fff]\ + style[content_search,content_cancel,go_back;bgimg="..assets.."btn_bg_2_dark.png;bgimg_middle=8,8]\ + image_button[0.125,0.125;0.75,0.75;"..assets.."search.png;content_search;]\ + tooltip[content_search;Search;#444;#aaa]\ + image_button[1,0.125;0.75,0.75;"..assets.."cancel.png;content_cancel;]\ + tooltip[content_cancel;Cancel;#444;#aaa]\ + style[test;bgimg="..assets.."white.png;bgimg_middle=0;bgcolor=#403e39ff]\ + style[test:hovered;bgimg="..assets.."white.png;bgimg_middle=0]\ + image[2,0.125;"..(size.x -9)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\ + field[2.1,0.125;"..(size.x -9.2)..",0.75;content_filter;;]\ + button["..(w -6)..",0;4,1;test;Browse Online Content...]\ + image_button["..(w -1.5)..",0.125;0.75,0.75;"..assets.."new_package.png;new_package;]\ + tooltip[new_package;New Package...;#444;#aaa]\ + " + + local start = w /2 -(pages *0.125) + + for i = 1, pages do + fs = fs.."\ + image_button["..(start +(i *0.25))..","..(size.y -0.5)..";0.25,0.25;"..assets..(i == page and "circle_light.png" or "circle.png")..";page"..i..";]\ + " + end + + else + + local pkg = state.all_content[state.current_package] + local desc = "" + if file_exists(pkg.path.."/README.md") then + desc = read_file(pkg.path.."/README.md") + end + local pkg_type = pkg.type + if pkg_type == "mod" then + pkg_type = "" + elseif pkg_type == "modpack" then + pkg_type = "" + elseif pkg_type == "game" then + pkg_type = "" + elseif pkg_type == "txp" then + pkg_type = "" + end + fs = fs.."\ + style[go_back;bgimg="..assets.."btn_bg_2_dark.png;bgimg_middle=8,8]\ + image_button["..(size.x -1)..",0.5;0.5,0.5;"..assets.."cancel.png;go_back;]\ + imdage["..(size.x *0.1 -0.1)..","..(size.y *0.1 -0.1)..";"..(size.x *0.6)..","..(size.y *0.8 +0.2)..";"..assets.."btn_bg.png;8,8]\ + hypertext["..(size.x *0.1)..","..(size.y *0.1)..";"..(size.x *0.6 -0.2)..","..(size.y *0.8)..";aa;\ + \ + "..markdown_to_hypertext(desc).."\ + ]\ + image["..(size.x *0.7 +0.1)..","..(size.y *0.1 -0.1)..";"..(size.x *0.2 +0.2)..","..(size.y *0.8 +0.2)..";"..assets.."btn_bg.png;8,8]\ + hypertext["..(size.x *0.7 +0.2)..","..(size.y *0.1)..";"..(size.x *0.2)..","..(size.y *0.8)..";bb;\ + \ + \ +
"..fe(hte(pkg.title or pkg.name)).."
\ + "..pkg_type.." \ + Creator: "..fe(hte(pkg.author)).."\ + Location: "..(pkg.path:sub(2):gsub("/", "")).."\ + "..(pkg.type == "modpack" and "Modules: "..#minetest.get_dir_list(pkg.path, true) or "").."\ + ]\ + button["..(size.x *0.7 +0.2)..","..(size.y *0.9 -0.75)..";"..(size.x *0.2)..",0.75;browse_source;Browse Source]\ + " + end + + fs = fs.."\ + button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\ + " + + minetest.update_formspec(content_header..fs) +end + +function show_about_menu() + if not state.lua_api then + state.lua_api = {} + state.lua_api_sections = {} + -- TODO: Make this portable + local doc = read_file(minetest.get_builtin_path().."/../luanti/lua_api.md") + local offset = 1 + while true do + local a, b, heading = doc:find("([^\n]+)\n=+\n", offset) + if not a then break end + if offset > 1 then + state.lua_api[state.lua_api_sections[#state.lua_api_sections]] = doc:sub(offset, a -1) + end + state.lua_api_sections[#state.lua_api_sections +1] = heading + offset = b + end + state.lua_api[state.lua_api_sections[#state.lua_api_sections]] = doc:sub(offset) + end + local fs = "\ + style[docs_section;bgimg="..assets.."white.png;bgcolor=#fff0]\ + style[docs_section:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\ + box[0,0;"..(size.x *0.25 -0.55)..","..size.y..";#403e39ff]\ + box["..(size.x *0.25 -0.55)..",0;0.1,"..size.y..";#292d2fff]\ + scroll_container[0,0;"..(size.x *0.25 -0.55)..","..size.y..";sidebarscroll;vertical;;0,0]\ + " + for i, x in ipairs(state.lua_api_sections) do + fs = fs.."\ + box[0,"..(i *0.8 -0.05)..";"..(size.x *0.25 -0.55)..",0.05;#292d2fff]\ + button[0,"..((i -1) *0.8)..";"..(size.x *0.25 -0.55)..",0.75;docs_section;"..fe(x).."]\ + " + end + fs = fs.."\ + scroll_container_end[]\ + scrollbar[-800,-800;0,0;vertical;sidebarscroll;"..(state.menu_vars.sidebarscroll and state.menu_vars.sidebarscroll:sub(5) or "").."]\ + hypertext["..(size.x *0.25)..",0;"..size.x..","..size.y..";aa;\ + "..markdown_to_hypertext(state.lua_api[state.menu_vars.docs_section or state.lua_api_sections[1]]).."\ + ]" + + fs = fs.."\ + button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\ + " + + minetest.update_formspec(content_header..fs) +end + +function show_settings_menu() + if not state.settings then + state.settings = {} + state.settings_categories = {} + local settings = settingtypes.parse_config_file(minetest.get_builtin_path().."/settingtypes.txt") + for _, x in ipairs(settings) do + if x.type == "category" then + if x.level == 1 and not x.name:find "Hide:" then + state.settings_categories[#state.settings_categories +1] = x.name + state.settings[x.name] = {} + end + else + local s = state.settings[state.settings_categories[#state.settings_categories]] + s[#s +1] = x + end + end + end + + local fs = "\ + box[0,0;"..(size.x *0.25 -0.75)..","..size.y..";#403e39ff]\ + box["..(size.x *0.25 -0.75)..",0;0.1,"..size.y..";#292d2fff]\ + scroll_container[0,0;"..(size.x *0.25 -0.75)..","..size.y..";sidebarscroll;vertical;;0,0]\ + style[show_theme;bgimg="..assets.."white.png;bgcolor=#fff0]\ + style[show_theme:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\ + button[0,0;"..(size.x *0.25 -0.55)..",0.75;show_theme;Theme]\ + " + + for i, category in ipairs(state.settings_categories) do + fs = fs.."\ + style[show_category;bgimg="..assets.."white.png;bgcolor=#fff0]\ + style[show_category:hovered;bgimg="..assets.."white.png;bgcolor=#fff1]\ + box[0,"..(i *0.8 -0.05)..";"..(size.x *0.25 -0.55)..",0.05;#292d2fff]\ + button[0,"..(i *0.8)..";"..(size.x *0.25 -0.55)..",0.75;show_category;"..fe(category).."]\ + " + end + + fs = fs.."scroll_container_end[]\ + scrollbar[-800,-800;0,0;vertical;sidebarscroll;]\ + scroll_container["..(size.x *0.25)..",0;"..(size.x *0.75)..","..size.y..";settingsscroll;vertical;;0,0]\ + " + + if state.settings_category then + local page = state.settings[state.settings_category] + local y = 0.25 + for i, x in ipairs(page) do + if x.type == "string" then + fs = fs.."label[0,"..y..";"..fe(x.readable_name).."]\ + image[0,"..(y +0.2)..";"..(size.x *0.3)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\ + field[0.1,"..(y +0.2)..";"..(size.x *0.3 -0.2)..",0.75;setting_"..fe(x.name)..";;"..(minetest.settings:get(x.name) or x.default).."]\ + button["..(size.x *0.75 -3)..","..(y +0.2)..";2,0.75;apply_setting_"..fe(x.name)..";Apply]\ + " + y = y +1.2 + elseif x.type == "bool" then + local state = minetest.settings:get_bool(x.name, x.default) + fs = fs.."image_button[0,"..y..";0.75,0.75;"..assets..(state and "checkbox_filled" or "checkbox_empty")..".png;toggle_setting_"..fe(x.name)..";]\ + label[1,"..(y +0.375)..";"..fe(x.readable_name).."]\ + " + y = y +1 + elseif x.type == "int" or x.type == "float" then + fs = fs.."label[0,"..y..";"..fe(x.readable_name).."]\ + image[0,"..(y +0.2)..";"..(size.x *0.3)..",0.75;"..assets.."btn_bg_2_light.png;8,8]\ + field[0.1,"..(y +0.2)..";"..(size.x *0.3 -0.2)..",0.75;setting_"..fe(x.name)..";;"..(minetest.settings:get(x.name) or x.default).."]\ + button["..(size.x *0.75 -3)..","..(y +0.2)..";2,0.75;apply_setting_"..fe(x.name)..";Apply]\ + " + y = y +1.2 + end + end + end + + + fs = fs.."\ + scroll_container_end[]\ + scrollbar[-800,-800;0,0;vertical;settingsscroll;]\ + button[0,"..(size.y -1)..";1,1;to_meta_menu;<]\ + " + + minetest.update_formspec(content_header..fs) +end + + +function minetest.button_handler(data) + if not state.menu_vars then state.menu_vars = {} end + setmetatable(state.menu_vars, {__index = data}) + if state.loc == "games" then + if data.content or data.ht == "action:content" then + if state.loc ~= "content" then show_content() end + elseif data.games then + if state.loc ~= "games" then show_meta_menu() end + elseif data.carousel and data.carousel:sub(1, 4) == "CHG:" then + local v = tonumber(data.carousel:sub(5)) + show_meta_menu(v) + else + for k, v in pairs(data) do + if k:sub(1, 4) == "game" then + local id = k:sub(5) + state.current_game = get_game_info(id) + state.menu_current = {"main"} + state.loc = "game" + state.menu_vars = {} + show_game_menu() + minetest.settings:set("menu_last_game", id) + last_game = id + elseif k:sub(1, 4) == "jump" then + local v = tonumber(k:sub(5)) + show_meta_menu(v) + end + end + end + elseif state.loc == "servers" then + if data.servers_refresh then + refresh_server_list() + elseif data.servers_search or data.key_enter_field == "servers_filter" then + state.servers_filter = data.servers_filter + state.serverlist_filtered = search_server_list(data.servers_filter) + show_servers_menu() + elseif data.servers_cancel then + state.serverlist_filtered = nil + state.servers_filter = nil + data.servers_filter = "" + show_servers_menu() + elseif data.unfavorite_server then + state.current_server.favorite = nil + remove_favorite_server(state.current_server.address, state.current_server.port) + show_servers_menu() + elseif data.show_server_mods then + state.showing_server_mods = true + show_servers_menu() + elseif data.show_server_players then + state.showing_server_players = true + show_servers_menu() + elseif data.close_dialog then + state.showing_server_players = nil + state.showing_server_mods = nil + show_servers_menu() + elseif data.direct_connection then + state.connecting_to_server = true + show_servers_menu() + elseif data.join_server then + state.joining_server = true + show_servers_menu() + elseif data.cancel_join_server then + state.joining_server = nil + state.connecting_to_server = nil + show_servers_menu() + elseif data.confirm_join_server or data.key_enter_field == "server_username" or data.key_enter_field == "server_password" or data.key_enter_field == "server_address" then + minetest.settings:set("name", data.server_username) + local address + local port + if state.connecting_to_server then + if not tonumber(data.server_port) then + state.server_connection_error = { + msg = "Invalid port.", + element = "server_port" + } + show_servers_menu() + state.server_connection_error = nil + return + end + address = data.server_address:lower() + port = data.server_port:lower() + else + address = state.current_server.address:lower() + port = state.current_server.port + end + local is_favorite = false + for _, x in ipairs(state.favorite_servers) do + if x.address == address and x.port == port then + is_favorite = true + end + end + if not is_favorite then + add_favorite_server(address, port) + end + minetest.settings:set("address", address) + minetest.settings:set("remote_port", port) + gamedata = { + playername = data.server_username, + password = data.server_password, + address = address, + port = port, + selected_world = 0, + singleplayer = false + } + + minetest.start() + else + for k, v in pairs(data) do + if k:sub(1, string.len("serverinfo")) == "serverinfo" then + local idx = k:sub(string.len("serverinfo>")) + if idx:sub(1, 1) == "f" then + state.current_server = state.favorite_servers[tonumber(idx:sub(2))] + else + state.current_server = (state.serverlist_filtered or state.serverlist)[tonumber(idx)] + end + show_servers_menu() + end + end + end + elseif state.loc == "content" then + if data.contentscroll and data.contentscroll:sub(1, 4) == "CHG:" then + state.content_page = tonumber(data.contentscroll:sub(5)) + show_content_menu() + elseif data.go_back then + state.current_package = nil + show_content_menu() + else + for k, v in pairs(data) do + if k:sub(1, 4) == "page" then + state.content_page = tonumber(k:sub(5)) + show_content_menu() + elseif k:sub(1, string.len("view_package_")) == "view_package_" then + state.current_package = tonumber(k:sub(string.len("view_package_>"))) + show_content_menu() + end + end + end + elseif state.loc == "game" then + for k, v in pairs(data) do + if k == ".play" then + gamedata = { + playername = "singleplayer", + password = "", + address = nil, + port = nil, + selected_world = get_world_index(state.menu_vars.selected_world), + singleplayer = true + } + minetest.start() + elseif k:sub(1, string.len(".show_dialog_")) == ".show_dialog_" then + show_game_menu { + show_dialog = k:sub(string.len(".show_dialog_>")) + } + elseif k:sub(1, string.len(".overlay_dialog_")) == ".overlay_dialog_" then + show_game_menu { + overlay_dialog = k:sub(string.len(".overlay_dialog_>")) + } + elseif k:sub(1, string.len(".select_world_")) == ".select_world_" then + state.menu_vars.selected_world = k:sub(string.len(".select_world_>")) + state.menu_vars.selected_world_name = k:match "/([^/]+)$" or "" + local conf = Settings(state.menu_vars.selected_world.."/world.mt") + local de = conf:get("enable_damage") + if de == "true" then + state.menu_vars.damage_enabled = true + elseif de == "false" then + state.menu_vars.damage_enabled = false + else + state.menu_vars.creative_damage = not state.current_game.disabled_settings.damage + end + local cm = conf:get("creative_mode") + if cm == "true" then + state.menu_vars.creative_enabled = true + elseif cm == "false" then + state.menu_vars.creative_enabled = false + else + state.menu_vars.creative_enabled = not not state.current_game.disabled_settings.creative + end + show_game_menu() + elseif k == ".unoverlay_dialog" then + show_game_menu { + 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 + show_game_menu() + end + end + elseif state.loc == "about" then + if data.docs_section then + show_about_menu() + end + elseif state.loc == "settings" then + if data.show_category then + state.settings_category = data.show_category + show_settings_menu() + else + for k, v in pairs(data) do + + end + end + end + if data.to_meta_menu then + state.current_game = nil + state.loc = "games" + show_meta_menu() + elseif data.show_servers_view then + state.current_game = nil + state.loc = "servers" + show_servers_menu() + elseif data.show_content_view then + state.current_game = nil + state.loc = "content" + show_content_menu() + elseif data.show_about_view then + state.current_game = nil + state.loc = "about" + show_about_menu() + elseif data.show_settings_view then + state.current_game = nil + state.loc = "settings" + show_settings_menu() + end +end + +function minetest.event_handler(ev) + -- When Esc is pressed, close the current dialog, or the game if we are on the meta menu. + if ev == "MenuQuit" then + if state.joining_server or state.connecting_to_server or state.showing_server_mods or state.showing_server_players then + state.joining_server = nil + state.connecting_to_server = nil + state.showing_server_mods = nil + state.showing_server_players = nil + show_servers_menu() + elseif state.loc ~= "games" then + show_meta_menu() + else + minetest.close() + end + end +end + +state.theme = build_stylesheet("") + +minetest.set_clouds(false) + +if last_game then + state.current_game = get_game_info(last_game) + if state.current_game then + state.menu_current = {"main"} + state.loc = "game" + show_game_menu() + else + show_meta_menu() + end +else + show_meta_menu() +end + +--]] diff --git a/logo_3d.png b/logo_3d.png new file mode 100644 index 0000000000000000000000000000000000000000..69a03859649faadad3e0575cd34077010a019e8a GIT binary patch literal 631 zcmV--0*L*IP)Px%F-b&0RCt{2+`noRK^Oq=Jz3M+B#_285V7(Fg7^nbkrz21PPWlHe&D%tRw+nKm%uW%ihLa_ikrrZ_Mvk?d6u)@4K1VZ*Ok1qpZfwS1Xh0 z_Pcw(Ms@oQ?aa^tbd`Jp4DH0^eE4m3^7mca*|~5Ob;6GxjK9tVyuCi|)ZpFkFN^IM zXRo~cVjBF$yrLa_bvkr|u~+btA416| zkUXo?LrXpZ0DumHUsX2UFxAJ`&EzlUl24$QA+)*sJA1ozwsF5u!>N}Z_*d{DB|n7n z7W>R)@@2pFz311S7TY`7UXbHO?}4rNAA+_QJ%QaDSA({f{2Yk7zH@wdtEkfX+Fp=j zx1PkYFA>^moxqSm0zjR{|6)?S08WPx_puFiFI@6p>XT0ZfbLY{0VSURbQFBu=Wzdi ztFzJCl^1b;V(RM6!D=tq|yUCwMgC`K;2|4rVr3dPg=wlc)PU1dRs;5aj z3ei}WWqJA6^QxZ*=Zo#h_;&4n<-1TWUH=g%w081xkc-$}A7`^|L{6Z3ey$s*O;10o zwh{B0Tqh>+yai{--~j*t00000000000000000000000000000000013#2-y}kasTX RhBg2I002ovPDHLkV1fwWDk}g0 literal 0 HcmV?d00001 diff --git a/main_bg.png b/main_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..9016473856cb51108cfdb7346d64bd55e91bdab7 GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D%KdAc};So9`; xnSJ!z{6-;%m5e?jH|??xtYBQs7I2<{A$l3}kIA}|jsTT1c)I$ztaD0e0sz-t9Wwv` literal 0 HcmV?d00001 diff --git a/menu_content.png b/menu_content.png new file mode 100644 index 0000000000000000000000000000000000000000..3c077dc43798fd808b3ae645ef095f4b5a5426de GIT binary patch literal 285 zcmV+&0pk9NP)Px#*GWV{R9J=WmOT!_FbsuV!~r^W>0GJY|1cmivoLW3I#iZ|u@nEKiV%5If*S~8hEm+peJ7RFZU9%pi$mTV4mVV&L=V=UD?E6TWXr6&Ds(L5?)Z_UAU@{?= zz_KX>fL#EoF^yS4%mC7VD>1C-K@u@poDp}GY$XLaQlojAQPl(UG-d}4HMR7Fx(T^KBdxK>dPx$Gf6~2R9J=Wm%R?cFbsqp6b80VUAiN+@Bb_yv9mC-F;K}YHA)@l$4P^P;3i0` z+~=xJlX?LWZIyvBbL@F^^2NLg01=}Q!O6+|I8KimfXn%Gh(YXP<~7DlHnfyp z5Ae!|zN>P+-tGWP5&$ssBvuJDsGQ47VwC_Q+T%DqVS~!KoFw*c3U8W{>SA-H98S_0IRQHc@RBDSIRq*iT462iBg3sCyDz&gCsPx#^GQTOR9J=WRyz*EAPl8d50EKK=1S@P57SDWS(vy%Iz&bmHTbz8F_b4oLKtlG z{E3~L_;*~i1NZ%Lv>155o>d#y7Uex~D+%c3+qyKGIQ5j67|y_#SIEnKh~hqSCuhRM zfDbV#;>Kvr}H&L<*0*_%EuvyR-ceOKLv zSo<4yd_LxT%=EtQ`+EMjuXE(=nfFwNvoXwVoN|dnU>0LpDj$afN0-%vXZk17mww@w zxtW#D5t^_3+DQ3&ht-D(UXrhJZ7u~rQ*lsxPx$TS-JgR9J=WS3Pe6F${I8>(Gsfg&hh0|8D__orQ^wp(nY^Ir2G9Y$u#Lv`<1p zB=N`di*W${I+AA-B2Hb#Q<|+kO~cG_R{)4;22wS=IG>KE^Q*@>;dZ@zA!0R%kCyWh z#{yHWjD-ed=7;@m$n*X21X!2=fLSuJVqk;JB?=~13?SlkJe|M6$egm0@SM3_FQ1%+ z%z!8R`{S7nacA_MuQ#w_K^0-mIXTiY0GF$<0=N?3Ya0`wbl*U_Y7EA_Y%@6xs z{t)DEToc0qKxZ%To^h4HqPSHO3+`L66*4`38w7o_;XNUn$8KP}FYapdG(+t-N0+fHfNX_L?Qp^Js)J79g5x}Yh z%Yr#d51<&y)OUf#L@#QY6I2myz=Y}?te4T~!-SG(JxqT6Y4(pF16SZfHof?c00000 LNkvXXu0mjfg<-R1 literal 0 HcmV?d00001 diff --git a/menu_servers_icon_ping_0.png b/menu_servers_icon_ping_0.png new file mode 100644 index 0000000000000000000000000000000000000000..1c690617ff0e29cb682ac9ec3354ea0224a7dfd4 GIT binary patch literal 117 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|^gUf1Lo_BP zCoGWMQ2P1r`2#@E;@#BP*ytGQ{MlYuk5ghc=X91BMm(^b literal 0 HcmV?d00001 diff --git a/menu_servers_icon_ping_1.png b/menu_servers_icon_ping_1.png new file mode 100644 index 0000000000000000000000000000000000000000..3b87b07cfdc7ccdd03753ceb64d21f9fcff09b60 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|oIG6|Lo_BP zCoGWMQ2P1r`2#@E;@#BP*ytGQ{MmkTSjbdHGfs)woiiI>+V}`=Vu@j7W@bJ-|Ml;4 gkBhk5q%SZshzbg%Yrhu$4m6Fy)78&qol`;+08cb4;Q#;t literal 0 HcmV?d00001 diff --git a/menu_servers_icon_ping_2.png b/menu_servers_icon_ping_2.png new file mode 100644 index 0000000000000000000000000000000000000000..409d13d1bc0f6dd1a82333ee4af8982ee8cdc273 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|!aQ9ZLo_BP zCoGWMQ2P1r`2#?}B7C81m65}=1Rw~wdg0G`j>!pW30D87Kb*ht!|c<~<85{wR#04i z=)mdKI;Vst0C0de AGXMYp literal 0 HcmV?d00001 diff --git a/menu_servers_icon_ping_3.png b/menu_servers_icon_ping_3.png new file mode 100644 index 0000000000000000000000000000000000000000..b728e2a9942d4c2093fdc9491274ee4656e1d7fb GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|B0OCjLo_BP zCoHhK(6!1a0SGpfe*Sy@01$NO9XxZOja>lvCK0k|V?^l*+&n)mDa~I63P3v;JYD@<);T3K0RSED BJ3;^e literal 0 HcmV?d00001 diff --git a/menu_servers_icon_ping_4.png b/menu_servers_icon_ping_4.png new file mode 100644 index 0000000000000000000000000000000000000000..a7344c415d11550f6be50b95cb843ed4c2570194 GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|yggkULo_BP zCn$(#xQ0p^fWV_k326yd|EE8kugI;($;QUkW`F(r-1LL02iLqjbN~d7oH)+=VN1tr*W^!e;V_-P+TBb*KS9w0rSO!m5KbLh*2~7ZzoH0xQ literal 0 HcmV?d00001 diff --git a/menu_servers_mods.png b/menu_servers_mods.png new file mode 100644 index 0000000000000000000000000000000000000000..ad04c7f4d5eb8cadd74df2723d9f0dc34a062ba3 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Hh8)?hFJ8T zon*_`Y{1hRAHeR`#^>Xsa4@8Eb%EcyrXyFp&Q3YW?3_P=@6U;dvRi`vAAhgelg>PS z-Lkhym&**7?u=Amm=e0z-j$(Z|2)5}xvX{X`X_IQNDKHdq4|z|L6*lc*3{7ECijhJ z)XwC*lzj4+VU}A%r)I^4Yt0ht+Gcz|%;op^qw(pmW&4c08GIsS7yR`9%EukO>a`kA uK=flx=4%t$S-AdvS3LPBea*q^EA(eo1exrgBP9WJJAN_fCBEA;1A)>1Z@sU3EO;9-|0ygwe*g#+7)9M5 zFP3O}$dtlxxr5(8FvO8z!48I3TmB10{I=WX>lVLAJh(K#$&a=5|D%htYhKOxkoiwx(fJRKF7m4zD|j0+ zj~!FH#gN2!l36M`KN-ne3*(9XCnPx#!%0LzR5*>@kueK`U=W5MC2=)Shooo;-TVcvZvNV~)_y^o(h@}t733O1UrWlT zoqzn_fS)k(qh@ z_vWhkGfw(xG>Bagap2D2TEJ?;dVy&cQwF0iFVdQ&MBb@03#S(ng9R* literal 0 HcmV?d00001 diff --git a/menu_tab_content.png b/menu_tab_content.png new file mode 100644 index 0000000000000000000000000000000000000000..0a1d89b55a1d0536a77730364061518dee12b8e9 GIT binary patch literal 273 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}r#)R9Ln2z= zPIKgQFc5I{mN{_hSk$4v{LV?a$xqpIQk;&>*!GL{;{HjVm;e9qQ#Vz6_1gd|{rz>;Nf~^lYc4_n7#aOv;#coW3G`z*>+Z`A8u4KzyTFjFS)@S$B7#(O;W!&YL>@xMhZRVB# zrcJqZVD5)tt#|7mc+Oa#ZNBKG$Q{nir1M4!r5S&#(`q}e<(ikjOFF>(S+3#og&>>8 zM{JWqKFs1>8ad%dJ`;;-qU)!)0S33hEx5b8dvFaNoIucEA-D&33x3Ht=f3Y* z-@W($?OD64cGdG#?dsayJ!{s4D=SK)A`>A4002}Ou!QQ1^m`cwhzKv|17wn<7XfcB zsvrsg)WyDeFnaYer!WDlDgXeUbT8N-0O0P$WED<-z?&H+aDTMAgJ)WL~72iKD5hos)&V zbMD0V$`=C?6s+w80KCEeGhhH2Sp)z8tc|6*ma~?EJfDfZEt?U<-q@7Q-4^;M4nV-2 z??tsWbvB}Kx3#fz;&T_I`U}DLqW>`isVM$}I9m%+X(=dEh}%1wQoLnjXJe-lLZ+ag z5O9Q;@u^Bk{X>752~t@&J45+^KsPrxHa9Lddq;C12QM!#kew6A$;tYHV0H4ab2f5k zwR58WJIMczBVp=f;%EtVwzRjS_!HO2*xtoikc#S0qW>I!uhZ1s^1qqvoc<~6r9j{x z50Hb69r%BvIa`|jKWKkEf7AY|>+f^|e}eIe+uPVXszHrROoccE{)+!^(|;HDH(J@! z-PA@~!qWDoF)xJ(@$&NgL;9bd|H;&FGIbQUw|yZx3;nmV|B(Of{GY_X0&D#@B?lMJ zyMO2Wm+N2DKNa&SIaAj^Z`!pWr_pBmrC zAK)fj(@hiZA8L;o&nI2aLMvX6cUK3;b0NM9o(itT13}W$gzE6Dr}pW7@xCRc(;~qz zYv{idPvR6y*PZ!PR8?UEdjOeZB@ry^c#*H93Ng>zNnG1W=@Mm@M;Znl@lj}h9hJpA6Uex))A311LEPEwQVCXNnBssxDs@+AQ)&L6Co1=os&{!$?KkH|kfkP!98rVQ*OfoKU?aHZqOmA(SsaOqZ7mcZtW4GO!ei+-<0r zi!=0it7a9}7`Fq`{SptxEQz;=rx8dmz{zh(x(rJHB?vyjHOa0RWgncP>;F1kYf6|g z-itMTZ*HeT=*>IZ5pyt4>x5Y@N-eb;~+4z8AosqCW5JvEZcbQw;$QT^Q{_5h)z!&9f>9NpJt)37*X2ti*H zopV-A(tD}LJZe;YI05&oOG3XpOiY_lcC_9y`mX$-EcNji1kBqYD5lC0S~w1;<2S3m z#kWPsp_-`N#_Yv!ja>Vcd?KH>2WWV|^RaW3$wW?b$06BL($s~FQkhxT-25IgZ>7~V zt@{l!^mEcdexRbVeEn2KidIdw*fXB~`8)y!qLl)ssdd~Y(pi%SO?qej;2$JZmSmN} zA>@(BB$L$17K)i$2?OOQ!X8b_6by|_iz+T~`pMI5v}`5FC0B_In-x>;sjsmoTnDt> zN$C))g-M7;%stpFDj#81kWHl)m#9emhJ5pN-ZDc67Z zEtURasGQpw^Y{X{qF%7Un?6ErxbA4Y9fQa=a8NURQbo8+l_hv8fwQrRY-@Ie&$P}; zgOrI`p>`nx6pE24FX-!Y9spBhi(D{Z23)&H&TDKWIW921s#NH(K*W=XRPcdX0Av(@ zB_(T?;iZ2Aw;*k9!dzat76z@3xXCmvU_N8iH$G0l+dG732!mF9AitQe4q4f@y{L=h zw6$?)l?n2bqKK5VzYDXAEfVf1Y5AC{%hFDcb-Y?wjOF|ZYk$Q@QxN*0Del`j#(5A7 zSnsXem(RrsNQz%+=Wn3gjCNjY6LmB!3_pZl@(6fac&C}h&=i&E4{^Qk(Eais%AUtQcTxI<}Eef!=gc^b* z`QF}<%CeN)2y&8>6;X~cQ=oT}vT1_Nis%#A??yD*0x+etyDX8QA0)q8%C!I)S9ec% z6k_Mx7V`SIh7#5ctNu0YUjNS1s7G5g|ul z-r9EDeqyjxcVxEoapZ0CMecJ4X(pRtPs*}%Ft@)#=KBwicf~DFZKyV9?aFGpvF5qn z$eIRq5hw4BKjaxQTeFLV)Mc|Y%d*5iZF9gubyAp=8h*z!-K=quH{;zBT~Rs z77_RISZaF{rs5n55)>3YBSwBTJH`6hGjs@hE5XN#F?EgeEpPu+G#BRu=O`Os6xODD)_&{v>O+;!->bNp)31pcS??97XD{J!+rFh05MlP$epGdB#uo?Xu z3RYRS*D4$~Qv_mP#&Ugc`v5d0G29$QMV5lZgo(+#Yqg5v3<49zAJ z{^B4J^$hIBgt{wfz$7&8*+`k`;IK&ht~G?L5=y&=fKdj#KErEZPrv}VM3ww zSlI{Ir;#@Zaqk&emFSgq7mct6TpdMAxp~8>O^yk4JNou|p%Jq=bD+S40Jjh zE9>ewoSo?~NyrDE9#1|U>{m3$rJ>C#3>hoNBL)T{7{DbV`;dUXOExsnnL>hVy9tgE z?ypHDH8o2;@kWekqZPYu$0lyQBZjw893aVcKZzldj=sKKOtCC zi887hAQ4a6!?sS7ZF^3PPPMyq_&7!85w-Ufr3D#EC=}rM&i1hBs`nX&-_WN;bYjA8 z0jy3_JdqNo9`koaCqv;Q<&*>@=DC#)jS~lIRT>0`G^fPj;`udGX?Y2Rn~wGzTykW_ z_NSaj>`3I{{WA?8S*OGa`un_LVW~Wd1Q6IHlI_G5aGlDMb5gC29+ZI%QGAZF`BR{i z=ks(Jc*26u)`&BV%w4bxEV&pmg&@tw>4%CQb&_?&bcGosPfyQJC2C+HF$s?2u2SG3qbIVmcUyjwNWK$d%~ATKl#loS18#))7!ikJv%?lMtn?i68HL;KTK{4q zqn;xp*n48_aREpvO1_&a)FLhxPuhgh*TKP>bUILlqKLFjC8k3CFo`Q{b}1l8kEj21SHslpce2MIs^uoA9AHbDUhw8)RTPxUca!>>hSksF7S; zLsDzdcA0sXwK0Jl{^!X}{q{(lg`ZsaQ>NEzVnViSKe6|K;ObZOFh#1!Lx=-2*RXnz zeC!E9$(h2y8|m-x_$+XgF;~&M*PzT#b;k>#s^9@CAGPtTeEuUP^9Q zzU5787Kv}(>CZ>;$p&F@b_*wf#hfd~0(9D(E!V#Mmh%;!`gV__iOQc<3?*8GHU`23 zNgX3Lx295dqf+B}8gBEM9ZBXqP3R&h_wOi=i=lpLs35lf!7{kOLoO<*T_$FF5HIe2 zefw_4TU;D&?nKR&Kbi)EF0XgOZRrRboS!*Pw8B4w&M%FaUXfWFpHSROK6vbnDX(6O z%Qes%C;#vo$Af9@1k}Im+q@alZWl6Q);s-t@mB3OQbxu%_)TnQsqC5qf870ZbCWO( zJ+}EwdfhU4oSS;y!kQZV-1d93O5Ns+OtzoCx(W=xDA9RbZ2PYhiO$iA*6C!`-Wn-mDTVj{#LG`t;+L< zNTa03e%nXjst>ri|5?TPTK5|mL9e+JT`ps84V*_9 zdi@l!*tES;iXD%s7r0vaHRMjkJ@NtSrMVHi2>0o-Q%vvOX?Hk)HOW~> zQtCTju$r1MZMze0Tpxf6Wt^G7ajPQCvw0+@dYp|#w;j>&?p*&<6|iro>ylBUBzE~D zZLDYA_gwOuUk>F>MV}HDy96EcP^p~1sA}s5I+EXP4;ETz*CXz2f!fi|);Rbb=a1Bo zA>S8ryX$f&JVU7wL#DOlnb*tdfYo|OeuH*No7g2VUJSFSqda2t=Q26C%?1+6MQvIhH!V)gu)b1!G_ylXPLy^>c{9ktLLJB$&(QDAA+bDR zaBLaOD~G<54Fd~TA3v4XWSyGizWeO}zSH&j%kKdb^bNIldj7F?+M(?=tMK!l+PZ4~ z4CwY)lI&PyeIn?jg2k0!$C}PQzKCRn*_MxE#_Fw8S~}at7p(N6jcUSXeX&A((7Hu_ ziqc|uFkVKmCU>&Q<2k3^=W;|L{HmZVg`!~xtWTufbgq!zDL&3ZaVNI?5MLRtYCE*N z5_j0xuEr1))BXgj)O`)_ahhsvAKbSoRmBtCLFdyz*)4bWqBPNQ8lqBjEbqvB`IqfS zZOP);8}3&P{B{z0)I4#^AOg~-dtX$G;CLI7`L^Id`L&|63-qQ6eMGamH{VTX(rJqz zfPW>(r_BTT!@XpFBLj)pW>Pn34%h~ zcQ<3fSg+0>Mbbo~(Fh#k3gH4NW17*?^QwD;izCawpP2KJ%sm(L`yu&b!WO2z; z*wz>^UKH}v_EuSUy-2v1LQJ3rT$Dq_RuNx9;KA51qdW@P)ifo&U-XqAB+$sTQ^ z{+I_HCltBc#9?Js&8I?J2&~-tUQ2$n;swlko_-Ui1b>Dn`OxJ;(q~(dV)77MV>~bY zwuhHHU{aKsgw`(TYag`XbNJJY50soJ^8gc_-^gi!;c3%7(&yR0vjSU@qce`cGXI#w z-+cfK3Pj?|L6pDTEE%y@*>Ew&92*GOdg@M-<&uS);9hfuI+#!o#a_gfW4bcxW`^EI zqps@B#^5H~Prel?1oU7!=0)sf@XC+YMgdQ}Nu!%ndZcJ=0?l916>M_rzg8w@{_3Bt zPoNa!5tkh93Flx|Isswo<8$vQ?$7_Kg=JRPrmUe+B2XB#8!1Ji7v`p>p|k$un;qQ_ zghY_>%4)~lc_cyMwYqNs37UX&P zVXW_Szh=K5(I$ge!K0G746qmmo2#qb(Pec1n*Ds*QZ*dN;DrlcP|)oFpWxD)NcuIi zP~m6211_r{W&hAxPZ;UVQ9d)@1j7$?fxtL| z2`hv%*+apl&+Cf)QYT|+A0a76{6Ka%NEwQ8j8&QtN!jHxNZEP-h8G?3yRV%LeL#%HHKyKdR4GXea?uA z9|WgV_s4#@DwE=6-9*Fh6^|YLCEw@A-%PsC`#jd)Yb`MKii@?R=%6X7n|sP6FnITf zptBpzIY1(;oi1u^07Emz((2W|oX8~8fzK_9=b!qKc- z{2WM;Lkeg0PHDU3v7Z>X*UxYyxzJ~*3d4MR3#J^SNWJhn@031Von!r!pHYHMW{4@S zYrSrUAd;`x0S96XKgPN=7A$ig2U75aB!Xp3J7zenU40%MBl#l zbS6C(ce=Z@4GQfOp^adKS*k=F{5|#R zCy630=fVCyLsNO}$(NhcQ1jft`Sk%6&w0?-<6arru&qy90iE8`A&h8hTAz!usK&*R zdQH|_JO@XAS?zcc&Pg-2Jv-LolL}&=GLVYM99s4#zs3taT37#muj*lM=827SB`zA_ z!Z`=OgHdFNOG+ulRVR;6jADt^(?2-~GVsaAZHgFC*~k%DWv5S?c2M`YmwBKQxbB}& zwq6eJgfPnTemT5uG1?;F5^=hI$}c?SxnP(3VX6H{tL|7OABgrTy?IhNq-M#R^=+j= zfN?V+@Ngk?mURHCiw*MG5X`7t{$y>VVr-D5)!g)XBW6)3oY<~S)I(vl)0^L}b(`yF zqXW;^RT9x}t0{%sE5(Q?QPhh$qS}G@k99Qf8(PTERGLA!wC0RK@b8?!cv*QRa$6Bx zC9@U4;)?+WA@3XWvlC$tl&+_%n!Uq4T9*07SBgvys1oW~kn@X0BfR?jP6GRm$p~4t z(RR}jrr)2$!8$5dQ-VHsPsR4Dxk9b6NjwE53)R1#Tmr@`gDoZt*dtt2mPX_RjAUJ0 zDWnMt{OP~>u@Q6hQJNe2RqtC+MAi++<=~;s7PaSLB#SZ>q|<(4^}>EWJB8bQ+;~@8 z6kfwn{B-$?>fs?(SbFbgOqCsW{?nz|L@+ejM z;Oic^59}!M@YkK~xnJqwni_l7eCe61ih3gQnzfU&U%$vz`ac0Ixb;?o{ex}k7+2tJt z&s94j)QvOr0=OvUs)oTEEn~Je=^u4(3F!t^Z;m7sz5zJLan+TjhnyP$oXm{kK|8n$ z(eJNp3feu+>}M7{;>dL;wPw4k>HRyw7=&mZJnTl6YNXl9_UchqABf<@AyjM*7vJ6K z!glxe2A9X6wNWljZ$TW3?otU)r{18;zxF?>mm`ZO*$yabJDsD8_0Q78rqmL}Dv z^W&k34AJZXP~(JT419Lv-wel##iqyskR;}o@qy-#riHusRI!N{g8{u%B4u;1^so1f zC;1b904-$=@}UH&RfeS>pIU!5%eGjYPCd7|uQmQAl%0C@rKKo&6iywq`tiWFEq8i@ z^KMTR?Gtw!_oB!{kUFXqzof^)h&Z7nV|L}v)||0!QW0b*W!pl;4EyN4cJ8NTV!~Ak zOS4iRY7q7wcMsodsZhmSVev6DvNZjDl_*^U?SZG|s1*aFJoY07o-htnWF)O3jkAPV z6;An=uYN-oeICYsiN*9G@Gz>ptw9)0ue<}mI2Om zi8v09Z%>yP{Y$qzU17(y%Cr+w@wN*(*9#P=ZoE(}7eYNNxCnmUsL@0^-+q8zBh~Bx zFNjP1tbXboq`0+S4Sx~dZYPAfh=DOn@*R(@N7ib*-^-(Z`Mt;sSa-RzXNa>fE!hVk zS|68~Svp<17o)x@yW)+u;pLeU1Fsu@@P|Q8ZE`A~hlLwOQV1rt{Qqs9!`b~>zrnBt z{-Ks9(+!0plPu~orw(BL8g8S(#%fdR_$jO*@#&c#cRszM3A8tIsr`0df+$)FV>2Ip zWrOpxms{u2Dh4_o!jH{>z9Nk6Yh^sirqC_Xba=+8-hs3DY~9vTXGX5i6VaKZCYUVM z_W7FQR3ySbzM{r2jO&Q*e}tKjB%p<}R<%h$-f2G-v(D$8l)CX+QU|e(MZ#RtCZe}+ zLw9Nxa?OAFn0Zf=c^?mleB~yUE@EM^P5)XmAU5Ur5>sYYo$FC04-^amCqw@GCopFK z;uo7FzJ9`l!z*m2QGASqX&`#Dogw?in{s&5OElw4ty5o?MGKKfqXC>he-4q6RFtR| I`w;NI0F@<_9RL6T literal 0 HcmV?d00001 diff --git a/new_package.png b/new_package.png new file mode 100644 index 0000000000000000000000000000000000000000..4c0428894bf0d222a16db6683d8ecd6409b6c7d5 GIT binary patch literal 121 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|j6Gc(Lo9le z6D00AggSrTzpCw7eNEO3jjbsUcoSTn34GG$xwEfOc3ppx_ybmD6|pu(28K=2oKL<* SIQjy$GkCiCxvX?Gm< literal 0 HcmV?d00001 diff --git a/refresh.png b/refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..de27b1b5d05fb24ba273063b8544c262d595351c GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|3O!vMLo9mN zPIBaHFyLTW-WzXt@xQ&Bu>4~6NnH(>thR3o*d`*egyGETDkhoVW!Bj)`75m+h;}b4 z-M;wh+OxucA0E*D_UNVg;FVdQ&MBb@0IpU>KmY&$ literal 0 HcmV?d00001 diff --git a/search.png b/search.png new file mode 100644 index 0000000000000000000000000000000000000000..cbbd7f3ffd23ec4544416e8ca948bde34b2d9a3c GIT binary patch literal 170 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|QaoK8Lo9le z6BY; z;iv&qXTX<(4A(MLLXEo^Z + label[2,2;A label] + + + button[1,1;4,1;button;This is a button] + +``` +will define a dialog named 'main', with a label in it, and a dialog named 'other', +with a button in it. Every menu file must have a dialog named 'main', which serves +as the entry point of the game's menu. + +Note that to prevent dialog definitions from conflicting with the contents of +hypertext[] elements, they must not have leading whitespace before the opening +delimiter. + +Being able to define these other dialogs would be pretty pointless if they couldn't +be used for anything. Accordingly, you can use standard formspec actions, e.g. buttons, +to segue to a different dialog. To do this, set the action name to +'.show_dialog_', where is the name of the dialog you want to open. +You can also use '.overlay_dialog_' to draw the target dialog on top of the +current dialog, and '.unoverlay_dialog' to hide it again. + +There are several other action patterns that invoke special behavior: + - + - '.play': Start the game. + - '.play_with_world_named_': Start the game on the specified world. If the + world in question does not exist, it will be created. + +It is also possible to define metadata for the main menu. To do this, create a dialog +section named 'meta'. The contents of this section will then be parsed in the same way +as minetest.conf instead of being treated as a dialog. + +Available metadata options are: + - 'enable_clouds': true/false (defaults to false) + + +Now, all this is good when we know exactly what content we want on the main menu. +But what if we want to render e.g. a list of worlds, or even a list of game modes? +This can be done using the @foreach construct. @foreach takes in a list of values, +the name of a JSON file, or a reference to a menu-exposed list. + +The major advantage of a foreach loop, however, is interpolation. Inside a foreach +loop, the pattern '${}' will be replaces with the result of . + +Inside an expression, you can reference variables as '@'. Which variables are +available depends on the iterated list. In the case of a list literal, the current +item is exposed as a variable named @item, while for a JSON file, each key in the +file's top-level object will correspond to a variable. (Note that variable names may +only contain letters or numbers, even if mapped from a JSON file.) + +The menu-provided lists are: + - @WORLDS: The list of worlds for this game. Exposes @name (the world name), + @path (the world's full absolute path), @gameid (the ID of the current game), + and @selected (whether the world is currently selected). + - @MODS: The list of installed mods that are not incompatible with this game. + Exposes @name, @title, @description, @author, @path, @depends, and @optional_depends. + - @WORLDMODS: The list of mods installed on the current world. No-op if no world + is selected. + +Expressions also support rudimentary mathematical operations, namely addition (+), +'subtraction' (+-), multiplication (*), division (/), and exponentiation (^). Trying +to perform math on a non-tonumber()-able variable will treat the variable as 0. + +As an example, this: +``` +label[1,1;Worlds:] +@foreach:$WORLDS:name + label[1,${@i + 1};World named ${@name}] +@endforeach:name + +@foreach:[one,two,three]:name2 + label[4,${@i + 1};Item ${@i} is named: ${@item}] +@endforeach:name2 +``` +will: + - Create a "Worlds" label at 1,1; + - Create a label for every world with that world's name and a Y coordinate that + corresponds to the world's position in the list, plus 1 so these labels start + below the existing label. + - Create labels for "one", "two", and "three" that display the item's value and + its position in the list, with similarly increasing Y-coordinates. + +Besides foreach loops, menu files also support conditionals. Conditionals are +written as '@if:: ... @else: ... @endif:', where + is any expression. The conditional will be replaced with the contents +of its first block if the condition evaluates to non-zero, and the contents of +its second block otherwise. Note that the second block may not be omitted due to +how the parser works. + +Example: +``` +@if: @name = test : + +@else: + +@endif: +``` + +Notes: + - The only uniqueness requirements for the name of a block is that it not be + the name of a statment of the same type contained in the body of that block. + This is so that the parser knows which `end` belongs to which block without + having to manage state. + +Note: Because of the way the main menu works, image paths must be specified in full. +To make this non-painful, when referencing images use '$ASSET_PATH/' +instead of just the image name. $ASSET_PATH will be replaced with the actual path +to the game's menu/ directory, so images used by the main menu should be stored +there. You can use $ASSET_PATH in any context. Additionally, $DEFAULT_ASSET_PATH is +the path to the builtin assets folder, and $NO_IMAGE is the path to blank.png, in +case you need to stylistically unset a background image defined by a global style. +--]] + +local 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 = {} + +function template.build_expr(expr) + return {type = "expr", value = expr} +end + +function template.build(menu) + -- First pass: Collect all tokens into a flat list + local block = {} + local item + local offset = 1 + local pos = 1 + while true do + local matched = false + local stmt_start, stmt_end, stmt = menu:find("{%%%s*(.-)%s*%%}", pos) + local expr_start, expr_end, expr = menu:find("{{(.-)}}", pos) + local comment_start, comment_end, comment = menu:find("{#(.-)#}", pos) + + -- Statements + if stmt_start and 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) + + local verb = stmt:match "^(%a+)" + if verb == "if" then + item = { + type = "if", + parent = block, + conditions = {{condition = template.build_expr(stmt:match "if%s*(.*)"), body = {}}} + } + block[#block +1] = item + block = item.conditions[1].body + elseif verb == "elseif" then + local x = { + condition = template.build_expr(stmt:match "if%s*(.*)"), + body = {} + } + item.conditions[#item.conditions +1] = x + block = x.body + elseif verb == "else" then + local x = { + condition = template.build_expr("true"), + body = {} + } + item.conditions[#item.conditions +1] = x + block = x.body + elseif verb == "endif" then + block = item.parent + elseif verb == "for" then + + elseif verb == "endfor" then + + elseif verb == "macro" then + + elseif verb == "endmacro" then + + elseif verb == "set" then + + elseif verb == "endset" then + + elseif verb == "include" then + + end + print(verb) + + offset = stmt_end +1 + pos = stmt_end + matched = true + end + + -- Interpolations + if expr_start and expr_start < (stmt_start or math.huge) and expr_start < (comment_start or math.huge) then + block[#block +1] = menu:sub(offset, expr_start -1) + + block[#block +1] = template.build_expr(expr) + + offset = expr_end +1 + pos = expr_end + matched = true + end + + -- Comments + if comment_start and comment_start < (stmt_start or math.huge) and comment_start < (expr_start or math.huge) then + offset = expr_end + pos = expr_end + matched = true + end + + if not matched then + break + end + end + + return block +end + +function template.evaluate_expr(expr) + if expr.type == "" then + + else + return false + end +end + +function template.evaluate(dialog, vars) + local out = "" + for _, item in ipairs(dialog) do + if type(item) == "string" then + out = out..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) + break + end + end + elseif item.type == "for" then + + end + end + end + return out +end + +print(template.evaluate(template.build [[ + This is a {{blah}}. + + {% if foo %} + Foo + {% elseif bar %} + Bar + {% else %} + Baz + {% endif %} + + {% for x in [a,b,c] %} + The item is {{ x }}! + Uppercase: {{ x:upper() }} + {% endfor %} +]], { + foo = false +})) diff --git a/white.png b/white.png new file mode 100644 index 0000000000000000000000000000000000000000..341eb3abf56c48c8266af0182606729ee6252e6a GIT binary patch literal 95 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61SBU+%rFB|jKx9jP7LeL$-D$|Bt2amLo9le q|NQ@N&#c+dsd9C(l$$A!CND#1F0*Wf;erJ~MGT&