From 74d5334e7289bc1ddc3f5f6724a07f2e89c2e391 Mon Sep 17 00:00:00 2001 From: Looki2000 Date: Sun, 19 Feb 2023 21:41:32 +0100 Subject: [PATCH] Initial commit --- .clang-format | 33 + .clang-tidy | 5 + .dockerignore | 4 + .editorconfig | 9 + .gitattributes | 2 + .github/CONTRIBUTING.md | 183 + .github/ISSUE_TEMPLATE/bug_report.md | 32 + .github/ISSUE_TEMPLATE/feature_request.md | 25 + .github/PULL_REQUEST_TEMPLATE.md | 20 + .github/SECURITY.md | 20 + .github/workflows/android.yml | 52 + .github/workflows/build.yml | 271 + .github/workflows/cpp_lint.yml | 55 + .github/workflows/lua.yml | 72 + .github/workflows/macos.yml | 67 + .gitignore | 119 + .gitlab-ci.yml | 289 + .luacheckrc | 82 + .mailmap | 69 + AppImageBuilder.yml | 54 + CMakeLists.txt | 402 + Dockerfile | 71 + LICENSE.txt | 206 + README.md | 480 + android/.gitignore | 11 + android/app/build.gradle | 116 + android/app/src/main/AndroidManifest.xml | 75 + .../net/minetest/minetest/CustomEditText.java | 45 + .../net/minetest/minetest/GameActivity.java | 207 + .../net/minetest/minetest/MainActivity.java | 185 + .../net/minetest/minetest/UnzipService.java | 259 + .../java/net/minetest/minetest/Utils.java | 39 + .../app/src/main/res/drawable/background.png | Bin 0 -> 83 bytes android/app/src/main/res/drawable/bg.xml | 4 + .../app/src/main/res/layout/activity_main.xml | 33 + .../app/src/main/res/mipmap/ic_launcher.png | Bin 0 -> 5780 bytes android/app/src/main/res/values/strings.xml | 13 + android/app/src/main/res/values/styles.xml | 15 + android/app/src/main/res/xml/filepaths.xml | 3 + android/build.gradle | 37 + android/gradle.properties | 11 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + android/gradlew | 188 + android/gradlew.bat | 100 + android/icons/aux1_btn.svg | 143 + android/icons/camera_btn.svg | 108 + android/icons/chat_btn.svg | 96 + android/icons/chat_hide_btn.svg | 139 + android/icons/chat_show_btn.svg | 133 + android/icons/checkbox_tick.svg | 93 + android/icons/debug_btn.svg | 344 + android/icons/down.svg | 542 + android/icons/drop_btn.svg | 173 + android/icons/fast_btn.svg | 190 + android/icons/fly_btn.svg | 168 + android/icons/gear_icon.svg | 194 + android/icons/inventory_btn.svg | 509 + android/icons/joystick_bg.svg | 876 + android/icons/joystick_center.svg | 877 + android/icons/joystick_off.svg | 882 + android/icons/jump_btn.svg | 547 + android/icons/minimap_btn.svg | 159 + android/icons/noclip_btn.svg | 173 + android/icons/rangeview_btn.svg | 456 + android/icons/rare_controls.svg | 521 + android/icons/zoom.svg | 599 + android/keystore-minetest.jks | Bin 0 -> 2247 bytes android/native/build.gradle | 69 + android/native/jni/Android.mk | 301 + android/native/jni/Application.mk | 32 + android/native/src/main/AndroidManifest.xml | 1 + android/settings.gradle | 2 + builtin/async/game.lua | 59 + builtin/async/mainmenu.lua | 9 + builtin/client/chatcommands.lua | 74 + builtin/client/death_formspec.lua | 15 + builtin/client/init.lua | 12 + builtin/client/misc.lua | 7 + builtin/client/register.lua | 83 + builtin/common/after.lua | 50 + builtin/common/chatcommands.lua | 178 + builtin/common/filterlist.lua | 319 + builtin/common/information_formspecs.lua | 136 + builtin/common/misc_helpers.lua | 770 + builtin/common/mod_storage.lua | 19 + builtin/common/serialize.lua | 238 + builtin/common/strict.lua | 46 + builtin/common/tests/misc_helpers_spec.lua | 173 + builtin/common/tests/serialize_spec.lua | 189 + builtin/common/tests/vector_spec.lua | 465 + builtin/common/vector.lua | 368 + builtin/fstk/buttonbar.lua | 215 + builtin/fstk/dialog.lua | 88 + builtin/fstk/tabview.lua | 257 + builtin/fstk/ui.lua | 212 + builtin/game/async.lua | 22 + builtin/game/auth.lua | 185 + builtin/game/chat.lua | 1359 ++ builtin/game/constants.lua | 31 + builtin/game/deprecated.lua | 65 + builtin/game/detached_inventory.lua | 24 + builtin/game/falling.lua | 608 + builtin/game/features.lua | 46 + builtin/game/forceloading.lua | 126 + builtin/game/init.lua | 40 + builtin/game/item.lua | 680 + builtin/game/item_entity.lua | 333 + builtin/game/item_s.lua | 156 + builtin/game/knockback.lua | 46 + builtin/game/misc.lua | 266 + builtin/game/misc_s.lua | 93 + builtin/game/privileges.lua | 108 + builtin/game/register.lua | 625 + builtin/game/statbars.lua | 187 + builtin/game/static_spawn.lua | 23 + builtin/game/voxelarea.lua | 134 + builtin/init.lua | 67 + builtin/locale/__builtin.de.tr | 246 + builtin/locale/__builtin.it.tr | 259 + builtin/locale/template.txt | 246 + builtin/mainmenu/async_event.lua | 32 + builtin/mainmenu/common.lua | 246 + builtin/mainmenu/dlg_config_world.lua | 409 + builtin/mainmenu/dlg_contentstore.lua | 1062 + builtin/mainmenu/dlg_create_world.lua | 488 + builtin/mainmenu/dlg_delete_content.lua | 70 + builtin/mainmenu/dlg_delete_world.lua | 58 + builtin/mainmenu/dlg_register.lua | 123 + builtin/mainmenu/dlg_rename_modpack.lua | 73 + builtin/mainmenu/dlg_settings_advanced.lua | 1137 + builtin/mainmenu/dlg_version_info.lua | 172 + builtin/mainmenu/game_theme.lua | 203 + .../mainmenu/generate_from_settingtypes.lua | 136 + builtin/mainmenu/init.lua | 131 + builtin/mainmenu/pkgmgr.lua | 929 + builtin/mainmenu/serverlistmgr.lua | 252 + builtin/mainmenu/tab_about.lua | 195 + builtin/mainmenu/tab_content.lua | 237 + builtin/mainmenu/tab_local.lua | 393 + builtin/mainmenu/tab_online.lua | 427 + builtin/mainmenu/tab_settings.lua | 403 + .../mainmenu/tests/favorites_wellformed.txt | 29 + builtin/mainmenu/tests/serverlistmgr_spec.lua | 38 + builtin/profiler/init.lua | 80 + builtin/profiler/instrumentation.lua | 235 + builtin/profiler/reporter.lua | 280 + builtin/profiler/sampling.lua | 206 + builtin/settingtypes.txt | 2353 ++ client/serverlist/.gitignore | 2 + .../3d_interlaced_merge/opengl_fragment.glsl | 23 + .../3d_interlaced_merge/opengl_vertex.glsl | 7 + .../default_shader/opengl_fragment.glsl | 6 + .../shaders/default_shader/opengl_vertex.glsl | 11 + .../minimap_shader/opengl_fragment.glsl | 35 + .../shaders/minimap_shader/opengl_vertex.glsl | 15 + .../shaders/nodes_shader/opengl_fragment.glsl | 492 + .../shaders/nodes_shader/opengl_vertex.glsl | 272 + .../object_shader/opengl_fragment.glsl | 495 + .../shaders/object_shader/opengl_vertex.glsl | 181 + .../selection_shader/opengl_fragment.glsl | 12 + .../selection_shader/opengl_vertex.glsl | 14 + .../shadow_shaders/pass1_fragment.glsl | 13 + .../shadow_shaders/pass1_trans_fragment.glsl | 42 + .../shadow_shaders/pass1_trans_vertex.glsl | 50 + .../shaders/shadow_shaders/pass1_vertex.glsl | 43 + .../shadow_shaders/pass2_fragment.glsl | 23 + .../shaders/shadow_shaders/pass2_vertex.glsl | 9 + .../shaders/stars_shader/opengl_fragment.glsl | 6 + .../shaders/stars_shader/opengl_vertex.glsl | 4 + clientmods/preview/example.lua | 2 + clientmods/preview/examples/first.lua | 1 + clientmods/preview/init.lua | 206 + clientmods/preview/mod.conf | 1 + clientmods/preview/settingtypes.txt | 1 + cmake/Modules/FindCURL.cmake | 16 + cmake/Modules/FindGMP.cmake | 25 + cmake/Modules/FindGettextLib.cmake | 69 + cmake/Modules/FindJson.cmake | 25 + cmake/Modules/FindLua.cmake | 28 + cmake/Modules/FindLuaJIT.cmake | 53 + cmake/Modules/FindNcursesw.cmake | 204 + cmake/Modules/FindSQLite3.cmake | 9 + cmake/Modules/FindVorbis.cmake | 45 + cmake/Modules/FindZstd.cmake | 25 + cmake/Modules/GenerateVersion.cmake | 26 + .../Modules/MinetestFindIrrlichtHeaders.cmake | 18 + doc/Doxyfile.in | 44 + doc/README.android | 81 + doc/breakages.md | 8 + doc/builtin_entities.txt | 101 + doc/client_lua_api.txt | 1527 ++ doc/direction.md | 69 + doc/fst_api.txt | 174 + doc/lgpl-2.1.txt | 502 + doc/lua_api.txt | 9521 ++++++++ doc/main_page.dox | 8 + doc/menu_lua_api.txt | 382 + doc/minetest.6 | 139 + doc/minetestserver.6 | 2 + doc/mkdocs/build.sh | 41 + doc/mkdocs/docs/css/code_styles.css | 68 + doc/mkdocs/docs/css/extra.css | 15 + doc/mkdocs/docs/img/favicon.ico | 1 + doc/mkdocs/lua_highlight.patch | 9 + doc/mod_channels.png | Bin 0 -> 349332 bytes doc/protocol.txt | 110 + doc/texture_packs.txt | 260 + doc/world_format.txt | 596 + fonts/Arimo-Bold.ttf | Bin 0 -> 420920 bytes fonts/Arimo-BoldItalic.ttf | Bin 0 -> 417668 bytes fonts/Arimo-Italic.ttf | Bin 0 -> 421372 bytes fonts/Arimo-LICENSE.txt | 2 + fonts/Arimo-Regular.ttf | Bin 0 -> 436876 bytes fonts/Cousine-Bold.ttf | Bin 0 -> 288272 bytes fonts/Cousine-BoldItalic.ttf | Bin 0 -> 265104 bytes fonts/Cousine-Italic.ttf | Bin 0 -> 262524 bytes fonts/Cousine-LICENSE.txt | 2 + fonts/Cousine-Regular.ttf | Bin 0 -> 309040 bytes fonts/DroidSansFallbackFull-LICENSE.txt | 13 + fonts/DroidSansFallbackFull.ttf | Bin 0 -> 5398328 bytes games/devtest/.luacheckrc | 43 + games/devtest/LICENSE.txt | 4 + games/devtest/README.md | 51 + games/devtest/game.conf | 2 + games/devtest/menu/background.png | Bin 0 -> 160 bytes games/devtest/menu/header.png | Bin 0 -> 404 bytes games/devtest/menu/icon.png | Bin 0 -> 217 bytes games/devtest/mods/basenodes/init.lua | 351 + games/devtest/mods/basenodes/mod.conf | 2 + .../basenodes_dirt_with_grass_bottom.png | Bin 0 -> 187 bytes .../textures/basenodes_dirt_with_snow.png | Bin 0 -> 166 bytes .../basenodes_dirt_with_snow_bottom.png | Bin 0 -> 177 bytes .../textures/basenodes_snow_sheet.png | Bin 0 -> 166 bytes .../mods/basenodes/textures/default_apple.png | Bin 0 -> 102 bytes .../basenodes/textures/default_cobble.png | Bin 0 -> 340 bytes .../textures/default_desert_sand.png | Bin 0 -> 293 bytes .../textures/default_desert_stone.png | Bin 0 -> 584 bytes .../mods/basenodes/textures/default_dirt.png | Bin 0 -> 7303 bytes .../mods/basenodes/textures/default_grass.png | Bin 0 -> 697 bytes .../basenodes/textures/default_gravel.png | Bin 0 -> 171 bytes .../mods/basenodes/textures/default_ice.png | Bin 0 -> 369 bytes .../textures/default_junglegrass.png | Bin 0 -> 201 bytes .../textures/default_jungleleaves.png | Bin 0 -> 399 bytes .../basenodes/textures/default_jungletree.png | Bin 0 -> 730 bytes .../textures/default_jungletree_top.png | Bin 0 -> 714 bytes .../mods/basenodes/textures/default_lava.png | Bin 0 -> 172 bytes .../textures/default_lava_flowing.png | Bin 0 -> 91 bytes .../basenodes/textures/default_leaves.png | Bin 0 -> 883 bytes .../textures/default_mossycobble.png | Bin 0 -> 574 bytes .../textures/default_pine_needles.png | Bin 0 -> 648 bytes .../basenodes/textures/default_pine_tree.png | Bin 0 -> 604 bytes .../textures/default_pine_tree_top.png | Bin 0 -> 174 bytes .../textures/default_river_water.png | Bin 0 -> 496 bytes .../textures/default_river_water_flowing.png | Bin 0 -> 99 bytes .../mods/basenodes/textures/default_sand.png | Bin 0 -> 554 bytes .../mods/basenodes/textures/default_snow.png | Bin 0 -> 166 bytes .../basenodes/textures/default_snow_side.png | Bin 0 -> 152 bytes .../mods/basenodes/textures/default_stone.png | Bin 0 -> 313 bytes .../mods/basenodes/textures/default_tree.png | Bin 0 -> 659 bytes .../basenodes/textures/default_tree_top.png | Bin 0 -> 175 bytes .../mods/basenodes/textures/default_water.png | Bin 0 -> 302 bytes .../textures/default_water_flowing.png | Bin 0 -> 115 bytes .../dirt_with_grass/default_grass.png | Bin 0 -> 829 bytes .../dirt_with_grass/default_grass_side.png | Bin 0 -> 796 bytes .../devtest/mods/basenodes/textures/info.txt | 7 + games/devtest/mods/basetools/init.lua | 449 + games/devtest/mods/basetools/mod.conf | 2 + .../textures/basetools_bloodsword.png | Bin 0 -> 165 bytes .../textures/basetools_elementalsword.png | Bin 0 -> 177 bytes .../textures/basetools_firesword.png | Bin 0 -> 166 bytes .../textures/basetools_healdagger.png | Bin 0 -> 162 bytes .../textures/basetools_healsword.png | Bin 0 -> 170 bytes .../basetools/textures/basetools_icesword.png | Bin 0 -> 170 bytes .../basetools/textures/basetools_mesepick.png | Bin 0 -> 156 bytes .../textures/basetools_mesesword.png | Bin 0 -> 163 bytes .../basetools/textures/basetools_steelaxe.png | Bin 0 -> 131 bytes .../textures/basetools_steeldagger.png | Bin 0 -> 154 bytes .../textures/basetools_steelpick.png | Bin 0 -> 159 bytes .../textures/basetools_steelpick_l1.png | Bin 0 -> 190 bytes .../textures/basetools_steelpick_l2.png | Bin 0 -> 177 bytes .../textures/basetools_steelshears.png | Bin 0 -> 208 bytes .../textures/basetools_steelshovel.png | Bin 0 -> 140 bytes .../textures/basetools_steelsword.png | Bin 0 -> 163 bytes .../basetools/textures/basetools_stoneaxe.png | Bin 0 -> 130 bytes .../textures/basetools_stonepick.png | Bin 0 -> 155 bytes .../textures/basetools_stoneshears.png | Bin 0 -> 224 bytes .../textures/basetools_stoneshovel.png | Bin 0 -> 134 bytes .../textures/basetools_stonesword.png | Bin 0 -> 159 bytes .../textures/basetools_superhealsword.png | Bin 0 -> 192 bytes .../textures/basetools_titaniumsword.png | Bin 0 -> 160 bytes .../basetools/textures/basetools_usespick.png | Bin 0 -> 161 bytes .../textures/basetools_usessword.png | Bin 0 -> 133 bytes .../basetools/textures/basetools_woodaxe.png | Bin 0 -> 121 bytes .../textures/basetools_wooddagger.png | Bin 0 -> 139 bytes .../basetools/textures/basetools_woodpick.png | Bin 0 -> 149 bytes .../textures/basetools_woodshears.png | Bin 0 -> 212 bytes .../textures/basetools_woodshovel.png | Bin 0 -> 133 bytes .../textures/basetools_woodsword.png | Bin 0 -> 139 bytes games/devtest/mods/broken/init.lua | 11 + games/devtest/mods/broken/mod.conf | 2 + games/devtest/mods/bucket/init.lua | 27 + games/devtest/mods/bucket/mod.conf | 2 + games/devtest/mods/bucket/textures/bucket.png | Bin 0 -> 163 bytes .../mods/bucket/textures/bucket_lava.png | Bin 0 -> 168 bytes .../mods/bucket/textures/bucket_water.png | Bin 0 -> 168 bytes games/devtest/mods/chest/init.lua | 40 + games/devtest/mods/chest/mod.conf | 2 + .../mods/chest/textures/chest_chest.png | Bin 0 -> 163 bytes .../devtest/mods/chest_of_everything/init.lua | 136 + .../devtest/mods/chest_of_everything/mod.conf | 2 + .../textures/chest_of_everything_chest.png | Bin 0 -> 210 bytes games/devtest/mods/dignodes/init.lua | 37 + games/devtest/mods/dignodes/mod.conf | 2 + .../dignodes/textures/dignodes_choppy.png | Bin 0 -> 187 bytes .../dignodes/textures/dignodes_cracky.png | Bin 0 -> 193 bytes .../dignodes/textures/dignodes_crumbly.png | Bin 0 -> 172 bytes .../textures/dignodes_dig_immediate.png | Bin 0 -> 170 bytes .../mods/dignodes/textures/dignodes_none.png | Bin 0 -> 201 bytes .../dignodes/textures/dignodes_rating1.png | Bin 0 -> 94 bytes .../dignodes/textures/dignodes_rating2.png | Bin 0 -> 92 bytes .../dignodes/textures/dignodes_rating3.png | Bin 0 -> 93 bytes games/devtest/mods/experimental/commands.lua | 221 + games/devtest/mods/experimental/detached.lua | 29 + games/devtest/mods/experimental/init.lua | 23 + games/devtest/mods/experimental/items.lua | 105 + games/devtest/mods/experimental/mod.conf | 2 + .../textures/experimental_callback_node.png | Bin 0 -> 139 bytes .../textures/experimental_particle_sheet.png | Bin 0 -> 208 bytes .../experimental_particle_vertical.png | Bin 0 -> 308 bytes .../textures/experimental_tester_tool_1.png | Bin 0 -> 138 bytes .../devtest/mods/give_initial_stuff/init.lua | 37 + .../devtest/mods/give_initial_stuff/mod.conf | 3 + games/devtest/mods/initial_message/init.lua | 9 + games/devtest/mods/initial_message/mod.conf | 2 + games/devtest/mods/mapgen/init.lua | 104 + games/devtest/mods/mapgen/mod.conf | 3 + games/devtest/mods/modchannels/init.lua | 14 + games/devtest/mods/modchannels/mod.conf | 2 + games/devtest/mods/soundstuff/init.lua | 174 + games/devtest/mods/soundstuff/mod.conf | 2 + .../soundstuff/sounds/soundstuff_mono.ogg | Bin 0 -> 4362 bytes .../soundstuff/textures/soundstuff_eat.png | Bin 0 -> 113 bytes .../textures/soundstuff_node_blank.png | Bin 0 -> 83 bytes .../textures/soundstuff_node_climbable.png | Bin 0 -> 189 bytes .../textures/soundstuff_node_dig.png | Bin 0 -> 126 bytes .../textures/soundstuff_node_dug.png | Bin 0 -> 132 bytes .../textures/soundstuff_node_fall.png | Bin 0 -> 100 bytes .../textures/soundstuff_node_footstep.png | Bin 0 -> 120 bytes .../textures/soundstuff_node_place.png | Bin 0 -> 115 bytes .../textures/soundstuff_node_place_failed.png | Bin 0 -> 143 bytes .../textures/soundstuff_node_sound.png | Bin 0 -> 116 bytes games/devtest/mods/stairs/init.lua | 65 + games/devtest/mods/stairs/mod.conf | 3 + games/devtest/mods/testentities/armor.lua | 61 + games/devtest/mods/testentities/callbacks.lua | 78 + games/devtest/mods/testentities/init.lua | 3 + games/devtest/mods/testentities/mod.conf | 2 + .../textures/testentities_armorball.png | Bin 0 -> 1385 bytes .../textures/testentities_callback.png | Bin 0 -> 156 bytes .../textures/testentities_callback_step.png | Bin 0 -> 166 bytes .../textures/testentities_cube1.png | Bin 0 -> 130 bytes .../textures/testentities_cube2.png | Bin 0 -> 128 bytes .../textures/testentities_cube3.png | Bin 0 -> 124 bytes .../textures/testentities_cube4.png | Bin 0 -> 126 bytes .../textures/testentities_cube5.png | Bin 0 -> 126 bytes .../textures/testentities_cube6.png | Bin 0 -> 126 bytes .../textures/testentities_dungeon_master.png | Bin 0 -> 2855 bytes .../textures/testentities_sprite.png | Bin 0 -> 120 bytes .../textures/testentities_upright_sprite1.png | Bin 0 -> 114 bytes .../textures/testentities_upright_sprite2.png | Bin 0 -> 119 bytes games/devtest/mods/testentities/visuals.lua | 137 + games/devtest/mods/testfood/init.lua | 31 + games/devtest/mods/testfood/mod.conf | 2 + .../mods/testfood/textures/testfood_bad.png | Bin 0 -> 126 bytes .../mods/testfood/textures/testfood_bad2.png | Bin 0 -> 133 bytes .../mods/testfood/textures/testfood_good.png | Bin 0 -> 129 bytes .../mods/testfood/textures/testfood_good2.png | Bin 0 -> 145 bytes .../testfood/textures/testfood_replace.png | Bin 0 -> 135 bytes games/devtest/mods/testformspec/LICENSE.txt | 14 + games/devtest/mods/testformspec/callbacks.lua | 51 + .../devtest/mods/testformspec/dummy_items.lua | 14 + games/devtest/mods/testformspec/formspec.lua | 536 + games/devtest/mods/testformspec/init.lua | 3 + games/devtest/mods/testformspec/mod.conf | 2 + .../models/testformspec_character.b3d | Bin 0 -> 73433 bytes .../models/testformspec_chest.obj | 79 + .../textures/default_chest_front.png | Bin 0 -> 423 bytes .../textures/default_chest_inside.png | Bin 0 -> 102 bytes .../textures/default_chest_side.png | Bin 0 -> 375 bytes .../textures/default_chest_top.png | Bin 0 -> 423 bytes .../textures/testformspec_9slice.png | Bin 0 -> 5935 bytes .../textures/testformspec_animation.jpg | Bin 0 -> 4376 bytes .../textures/testformspec_animation.png | Bin 0 -> 214 bytes .../testformspec/textures/testformspec_bg.png | Bin 0 -> 92 bytes .../textures/testformspec_bg_9slice.png | Bin 0 -> 146 bytes .../testformspec_bg_9slice_hovered.png | Bin 0 -> 146 bytes .../testformspec_bg_9slice_pressed.png | Bin 0 -> 146 bytes .../textures/testformspec_bg_hovered.png | Bin 0 -> 92 bytes .../textures/testformspec_bg_pressed.png | Bin 0 -> 91 bytes .../textures/testformspec_button_image.png | Bin 0 -> 146 bytes .../textures/testformspec_character.png | Bin 0 -> 2754 bytes .../textures/testformspec_hovered.png | Bin 0 -> 195 bytes .../textures/testformspec_item.png | Bin 0 -> 182 bytes .../textures/testformspec_node.png | Bin 0 -> 113 bytes .../textures/testformspec_pressed.png | Bin 0 -> 165 bytes games/devtest/mods/testhud/init.lua | 81 + games/devtest/mods/testhud/mod.conf | 2 + games/devtest/mods/testitems/init.lua | 55 + games/devtest/mods/testitems/mod.conf | 2 + .../textures/testitems_overlay_base.png | Bin 0 -> 106 bytes .../textures/testitems_overlay_overlay.png | Bin 0 -> 220 bytes games/devtest/mods/testnodes/README.md | 11 + games/devtest/mods/testnodes/drawtypes.lua | 629 + games/devtest/mods/testnodes/init.lua | 11 + games/devtest/mods/testnodes/light.lua | 50 + games/devtest/mods/testnodes/liquids.lua | 134 + games/devtest/mods/testnodes/meshes.lua | 145 + games/devtest/mods/testnodes/mod.conf | 3 + .../testnodes/models/testnodes_ocorner.obj | 23 + .../testnodes/models/testnodes_pyramid.obj | 24 + games/devtest/mods/testnodes/nodeboxes.lua | 81 + games/devtest/mods/testnodes/overlays.lua | 93 + games/devtest/mods/testnodes/param2.lua | 168 + games/devtest/mods/testnodes/properties.lua | 397 + games/devtest/mods/testnodes/textures.lua | 290 + .../mods/testnodes/textures/testnodes_1.png | Bin 0 -> 107 bytes .../mods/testnodes/textures/testnodes_1g.png | Bin 0 -> 104 bytes .../mods/testnodes/textures/testnodes_1w.png | Bin 0 -> 121 bytes .../mods/testnodes/textures/testnodes_1wg.png | Bin 0 -> 122 bytes .../mods/testnodes/textures/testnodes_2.png | Bin 0 -> 112 bytes .../mods/testnodes/textures/testnodes_2g.png | Bin 0 -> 110 bytes .../mods/testnodes/textures/testnodes_2w.png | Bin 0 -> 134 bytes .../mods/testnodes/textures/testnodes_2wg.png | Bin 0 -> 135 bytes .../mods/testnodes/textures/testnodes_3.png | Bin 0 -> 105 bytes .../mods/testnodes/textures/testnodes_3g.png | Bin 0 -> 103 bytes .../mods/testnodes/textures/testnodes_3w.png | Bin 0 -> 112 bytes .../mods/testnodes/textures/testnodes_3wg.png | Bin 0 -> 112 bytes .../mods/testnodes/textures/testnodes_4.png | Bin 0 -> 97 bytes .../mods/testnodes/textures/testnodes_4g.png | Bin 0 -> 95 bytes .../mods/testnodes/textures/testnodes_4w.png | Bin 0 -> 128 bytes .../mods/testnodes/textures/testnodes_4wg.png | Bin 0 -> 128 bytes .../mods/testnodes/textures/testnodes_5.png | Bin 0 -> 98 bytes .../mods/testnodes/textures/testnodes_5g.png | Bin 0 -> 98 bytes .../mods/testnodes/textures/testnodes_5w.png | Bin 0 -> 117 bytes .../mods/testnodes/textures/testnodes_5wg.png | Bin 0 -> 117 bytes .../mods/testnodes/textures/testnodes_6.png | Bin 0 -> 100 bytes .../mods/testnodes/textures/testnodes_6g.png | Bin 0 -> 98 bytes .../mods/testnodes/textures/testnodes_6w.png | Bin 0 -> 117 bytes .../mods/testnodes/textures/testnodes_6wg.png | Bin 0 -> 117 bytes .../testnodes/textures/testnodes_airlike.png | Bin 0 -> 92 bytes .../testnodes/textures/testnodes_allfaces.png | Bin 0 -> 150 bytes .../textures/testnodes_allfaces_optional.png | Bin 0 -> 150 bytes .../testnodes/textures/testnodes_alpha.png | Bin 0 -> 96 bytes .../testnodes/textures/testnodes_alpha128.png | Bin 0 -> 136 bytes .../testnodes/textures/testnodes_alpha191.png | Bin 0 -> 132 bytes .../testnodes/textures/testnodes_alpha64.png | Bin 0 -> 134 bytes .../testnodes/textures/testnodes_anim.png | Bin 0 -> 274 bytes .../textures/testnodes_attached_bottom.png | Bin 0 -> 86 bytes .../textures/testnodes_attached_side.png | Bin 0 -> 98 bytes .../textures/testnodes_attached_top.png | Bin 0 -> 87 bytes .../textures/testnodes_attachedw_bottom.png | Bin 0 -> 130 bytes .../textures/testnodes_attachedw_side.png | Bin 0 -> 122 bytes .../textures/testnodes_attachedw_top.png | Bin 0 -> 109 bytes .../testnodes/textures/testnodes_bouncy.png | Bin 0 -> 106 bytes .../textures/testnodes_buildable_to.png | Bin 0 -> 89 bytes .../testnodes_climbable_nojump_side.png | Bin 0 -> 164 bytes .../testnodes_climbable_resistance_side.png | Bin 0 -> 295 bytes .../textures/testnodes_climbable_side.png | Bin 0 -> 150 bytes .../testnodes/textures/testnodes_damage.png | Bin 0 -> 108 bytes .../textures/testnodes_damage_neg.png | Bin 0 -> 121 bytes .../testnodes/textures/testnodes_drowning.png | Bin 0 -> 127 bytes .../textures/testnodes_fall_damage_minus.png | Bin 0 -> 116 bytes .../textures/testnodes_fall_damage_plus.png | Bin 0 -> 117 bytes .../textures/testnodes_fencelike.png | Bin 0 -> 90 bytes .../testnodes/textures/testnodes_firelike.png | Bin 0 -> 149 bytes .../textures/testnodes_glasslike.png | Bin 0 -> 117 bytes .../textures/testnodes_glasslike_detail.png | Bin 0 -> 167 bytes .../textures/testnodes_glasslike_framed.png | Bin 0 -> 88 bytes .../textures/testnodes_glasslike_framed2.png | Bin 0 -> 118 bytes .../testnodes_glasslike_framed_optional.png | Bin 0 -> 128 bytes .../textures/testnodes_glasslikeliquid.png | Bin 0 -> 122 bytes .../testnodes/textures/testnodes_light.png | Bin 0 -> 117 bytes .../testnodes/textures/testnodes_light_1.png | Bin 0 -> 158 bytes .../testnodes/textures/testnodes_light_10.png | Bin 0 -> 147 bytes .../testnodes/textures/testnodes_light_11.png | Bin 0 -> 149 bytes .../testnodes/textures/testnodes_light_12.png | Bin 0 -> 138 bytes .../testnodes/textures/testnodes_light_13.png | Bin 0 -> 137 bytes .../testnodes/textures/testnodes_light_14.png | Bin 0 -> 132 bytes .../testnodes/textures/testnodes_light_2.png | Bin 0 -> 152 bytes .../testnodes/textures/testnodes_light_3.png | Bin 0 -> 150 bytes .../testnodes/textures/testnodes_light_4.png | Bin 0 -> 157 bytes .../testnodes/textures/testnodes_light_5.png | Bin 0 -> 150 bytes .../testnodes/textures/testnodes_light_6.png | Bin 0 -> 154 bytes .../testnodes/textures/testnodes_light_7.png | Bin 0 -> 146 bytes .../testnodes/textures/testnodes_light_8.png | Bin 0 -> 139 bytes .../testnodes/textures/testnodes_light_9.png | Bin 0 -> 149 bytes .../textures/testnodes_line_crossing.png | Bin 0 -> 130 bytes .../textures/testnodes_line_curved.png | Bin 0 -> 123 bytes .../textures/testnodes_line_straight.png | Bin 0 -> 115 bytes .../textures/testnodes_line_t_junction.png | Bin 0 -> 128 bytes .../testnodes/textures/testnodes_liquid.png | Bin 0 -> 95 bytes .../textures/testnodes_liquidflowing.png | Bin 0 -> 132 bytes .../textures/testnodes_liquidflowing_r0.png | Bin 0 -> 162 bytes .../textures/testnodes_liquidflowing_r1.png | Bin 0 -> 160 bytes .../textures/testnodes_liquidflowing_r2.png | Bin 0 -> 154 bytes .../textures/testnodes_liquidflowing_r3.png | Bin 0 -> 155 bytes .../textures/testnodes_liquidflowing_r4.png | Bin 0 -> 154 bytes .../textures/testnodes_liquidflowing_r5.png | Bin 0 -> 155 bytes .../textures/testnodes_liquidflowing_r6.png | Bin 0 -> 155 bytes .../textures/testnodes_liquidflowing_r7.png | Bin 0 -> 157 bytes .../textures/testnodes_liquidflowing_r8.png | Bin 0 -> 152 bytes .../textures/testnodes_liquidsource.png | Bin 0 -> 128 bytes .../textures/testnodes_liquidsource_r0.png | Bin 0 -> 149 bytes .../textures/testnodes_liquidsource_r1.png | Bin 0 -> 152 bytes .../textures/testnodes_liquidsource_r2.png | Bin 0 -> 152 bytes .../textures/testnodes_liquidsource_r3.png | Bin 0 -> 152 bytes .../textures/testnodes_liquidsource_r4.png | Bin 0 -> 155 bytes .../textures/testnodes_liquidsource_r5.png | Bin 0 -> 150 bytes .../textures/testnodes_liquidsource_r6.png | Bin 0 -> 148 bytes .../textures/testnodes_liquidsource_r7.png | Bin 0 -> 154 bytes .../textures/testnodes_liquidsource_r8.png | Bin 0 -> 148 bytes .../textures/testnodes_mesh_stripes.png | Bin 0 -> 150 bytes .../textures/testnodes_mesh_stripes2.png | Bin 0 -> 144 bytes .../textures/testnodes_mesh_stripes3.png | Bin 0 -> 85 bytes .../textures/testnodes_mesh_stripes4.png | Bin 0 -> 93 bytes .../textures/testnodes_move_resistance.png | Bin 0 -> 221 bytes .../testnodes/textures/testnodes_node.png | Bin 0 -> 89 bytes .../textures/testnodes_node_falling.png | Bin 0 -> 112 bytes .../testnodes/textures/testnodes_nodebox.png | Bin 0 -> 112 bytes .../textures/testnodes_nojump_side.png | Bin 0 -> 94 bytes .../textures/testnodes_nojump_top.png | Bin 0 -> 121 bytes .../testnodes/textures/testnodes_normal.png | Bin 0 -> 92 bytes .../testnodes/textures/testnodes_normal1.png | Bin 0 -> 114 bytes .../testnodes/textures/testnodes_normal2.png | Bin 0 -> 122 bytes .../testnodes/textures/testnodes_normal3.png | Bin 0 -> 114 bytes .../testnodes/textures/testnodes_normal4.png | Bin 0 -> 106 bytes .../testnodes/textures/testnodes_normal5.png | Bin 0 -> 109 bytes .../testnodes/textures/testnodes_normal6.png | Bin 0 -> 111 bytes .../testnodes/textures/testnodes_overlay.png | Bin 0 -> 153 bytes .../textures/testnodes_overlayable.png | Bin 0 -> 87 bytes .../textures/testnodes_palette_facedir.png | Bin 0 -> 87 bytes .../textures/testnodes_palette_full.png | Bin 0 -> 568 bytes .../testnodes_palette_wallmounted.png | Bin 0 -> 136 bytes .../textures/testnodes_plantlike.png | Bin 0 -> 183 bytes .../testnodes_plantlike_degrotate.png | Bin 0 -> 176 bytes .../textures/testnodes_plantlike_leveled.png | Bin 0 -> 163 bytes .../testnodes_plantlike_meshoptions.png | Bin 0 -> 164 bytes .../textures/testnodes_plantlike_rooted.png | Bin 0 -> 182 bytes .../testnodes_plantlike_rooted_base.png | Bin 0 -> 101 bytes ...s_plantlike_rooted_base_side_degrotate.png | Bin 0 -> 145 bytes ...des_plantlike_rooted_base_side_leveled.png | Bin 0 -> 127 bytes ...plantlike_rooted_base_side_meshoptions.png | Bin 0 -> 128 bytes ...plantlike_rooted_base_side_wallmounted.png | Bin 0 -> 224 bytes ...odes_plantlike_rooted_base_side_waving.png | Bin 0 -> 126 bytes .../testnodes_plantlike_rooted_degrotate.png | Bin 0 -> 176 bytes .../testnodes_plantlike_rooted_leveled.png | Bin 0 -> 162 bytes ...testnodes_plantlike_rooted_meshoptions.png | Bin 0 -> 162 bytes ...testnodes_plantlike_rooted_wallmounted.png | Bin 0 -> 268 bytes .../testnodes_plantlike_rooted_waving.png | Bin 0 -> 125 bytes .../testnodes_plantlike_wallmounted.png | Bin 0 -> 268 bytes .../textures/testnodes_plantlike_waving.png | Bin 0 -> 127 bytes .../textures/testnodes_rail2_crossing.png | Bin 0 -> 246 bytes .../textures/testnodes_rail2_curved.png | Bin 0 -> 253 bytes .../textures/testnodes_rail2_straight.png | Bin 0 -> 246 bytes .../textures/testnodes_rail2_t_junction.png | Bin 0 -> 254 bytes .../textures/testnodes_rail_crossing.png | Bin 0 -> 151 bytes .../textures/testnodes_rail_curved.png | Bin 0 -> 164 bytes .../textures/testnodes_rail_straight.png | Bin 0 -> 133 bytes .../textures/testnodes_rail_t_junction.png | Bin 0 -> 202 bytes .../testnodes/textures/testnodes_signlike.png | Bin 0 -> 150 bytes .../testnodes/textures/testnodes_slippery.png | Bin 0 -> 111 bytes .../textures/testnodes_street_crossing.png | Bin 0 -> 137 bytes .../textures/testnodes_street_curved.png | Bin 0 -> 115 bytes .../textures/testnodes_street_straight.png | Bin 0 -> 101 bytes .../textures/testnodes_street_t_junction.png | Bin 0 -> 126 bytes .../textures/testnodes_sunlight_filter.png | Bin 0 -> 113 bytes .../testnodes_tga_type10_32bpp_bt.tga | Bin 0 -> 179 bytes .../testnodes_tga_type10_32bpp_tb.tga | Bin 0 -> 179 bytes .../textures/testnodes_tga_type1_24bpp_bt.tga | Bin 0 -> 120 bytes .../textures/testnodes_tga_type1_24bpp_tb.tga | Bin 0 -> 120 bytes .../textures/testnodes_tga_type2_16bpp_bt.tga | Bin 0 -> 172 bytes .../textures/testnodes_tga_type2_16bpp_tb.tga | Bin 0 -> 172 bytes .../textures/testnodes_tga_type2_32bpp_bt.tga | Bin 0 -> 300 bytes .../textures/testnodes_tga_type2_32bpp_tb.tga | Bin 0 -> 300 bytes .../textures/testnodes_tga_type3_16bpp_bt.tga | Bin 0 -> 172 bytes .../textures/testnodes_tga_type3_16bpp_tb.tga | Bin 0 -> 172 bytes .../textures/testnodes_torchlike_ceiling.png | Bin 0 -> 157 bytes .../textures/testnodes_torchlike_floor.png | Bin 0 -> 141 bytes .../textures/testnodes_torchlike_wall.png | Bin 0 -> 158 bytes games/devtest/mods/testpathfinder/README.md | 15 + games/devtest/mods/testpathfinder/init.lua | 136 + games/devtest/mods/testpathfinder/mod.conf | 2 + .../testpathfinder_testpathfinder.png | Bin 0 -> 159 bytes .../textures/testpathfinder_waypoint.png | Bin 0 -> 99 bytes .../textures/testpathfinder_waypoint_end.png | Bin 0 -> 147 bytes .../testpathfinder_waypoint_start.png | Bin 0 -> 139 bytes games/devtest/mods/testtools/README.md | 128 + games/devtest/mods/testtools/init.lua | 951 + games/devtest/mods/testtools/light.lua | 37 + games/devtest/mods/testtools/mod.conf | 2 + .../textures/testtools_children_getter.png | Bin 0 -> 281 bytes .../textures/testtools_entity_rotator.png | Bin 0 -> 151 bytes .../textures/testtools_entity_scaler.png | Bin 0 -> 182 bytes .../textures/testtools_entity_spawner.png | Bin 0 -> 189 bytes .../textures/testtools_falling_node_tool.png | Bin 0 -> 140 bytes .../textures/testtools_item_meta_editor.png | Bin 0 -> 114 bytes .../textures/testtools_lighttool.png | Bin 0 -> 1659 bytes .../textures/testtools_node_meta_editor.png | Bin 0 -> 135 bytes .../textures/testtools_node_setter.png | Bin 0 -> 146 bytes .../textures/testtools_object_attacher.png | Bin 0 -> 173 bytes .../textures/testtools_object_editor.png | Bin 0 -> 180 bytes .../textures/testtools_object_mover.png | Bin 0 -> 175 bytes .../textures/testtools_param2tool.png | Bin 0 -> 127 bytes .../testtools/textures/testtools_remover.png | Bin 0 -> 129 bytes games/devtest/mods/tiled/init.lua | 33 + games/devtest/mods/tiled/mod.conf | 3 + .../mods/tiled/textures/tiled_tiled.png | Bin 0 -> 410 bytes games/devtest/mods/unittests/async_env.lua | 168 + games/devtest/mods/unittests/crafting.lua | 112 + .../mods/unittests/crafting_prepare.lua | 94 + games/devtest/mods/unittests/entity.lua | 132 + games/devtest/mods/unittests/init.lua | 202 + .../mods/unittests/inside_async_env.lua | 25 + .../mods/unittests/itemdescription.lua | 42 + games/devtest/mods/unittests/misc.lua | 82 + games/devtest/mods/unittests/mod.conf | 3 + games/devtest/mods/unittests/player.lua | 70 + .../mods/unittests/textures/default_dirt.png | Bin 0 -> 790 bytes .../textures/unittests_coal_lump.png | Bin 0 -> 160 bytes .../textures/unittests_description_test.png | Bin 0 -> 268 bytes .../textures/unittests_iron_lump.png | Bin 0 -> 154 bytes .../textures/unittests_repairable_tool.png | Bin 0 -> 160 bytes .../textures/unittests_steel_ingot.png | Bin 0 -> 159 bytes .../unittests/textures/unittests_stick.png | Bin 0 -> 147 bytes .../unittests/textures/unittests_torch.png | Bin 0 -> 155 bytes .../textures/unittests_unrepairable_tool.png | Bin 0 -> 157 bytes games/devtest/mods/util_commands/init.lua | 309 + games/devtest/mods/util_commands/mod.conf | 2 + games/devtest/screenshot.png | Bin 0 -> 133364 bytes games/devtest/settingtypes.txt | 32 + lib/bitop/CMakeLists.txt | 4 + lib/bitop/bit.c | 189 + lib/bitop/bit.h | 34 + lib/catch2/CMakeLists.txt | 16 + lib/catch2/catch.hpp | 17970 ++++++++++++++++ lib/gmp/CMakeLists.txt | 3 + lib/gmp/mini-gmp.c | 4560 ++++ lib/gmp/mini-gmp.h | 293 + lib/jsoncpp/CMakeLists.txt | 3 + lib/jsoncpp/json/UPDATING | 7 + lib/jsoncpp/json/json-forwards.h | 447 + lib/jsoncpp/json/json.h | 2343 ++ lib/jsoncpp/jsoncpp.cpp | 5326 +++++ lib/lua/CMakeLists.txt | 59 + lib/lua/COPYRIGHT | 34 + lib/lua/src/CMakeLists.txt | 44 + lib/lua/src/lapi.c | 1087 + lib/lua/src/lapi.h | 16 + lib/lua/src/lauxlib.c | 652 + lib/lua/src/lauxlib.h | 174 + lib/lua/src/lbaselib.c | 653 + lib/lua/src/lcode.c | 831 + lib/lua/src/lcode.h | 76 + lib/lua/src/ldblib.c | 398 + lib/lua/src/ldebug.c | 638 + lib/lua/src/ldebug.h | 33 + lib/lua/src/ldo.c | 519 + lib/lua/src/ldo.h | 57 + lib/lua/src/ldump.c | 164 + lib/lua/src/lfunc.c | 174 + lib/lua/src/lfunc.h | 34 + lib/lua/src/lgc.c | 715 + lib/lua/src/lgc.h | 110 + lib/lua/src/linit.c | 38 + lib/lua/src/liolib.c | 556 + lib/lua/src/llex.c | 469 + lib/lua/src/llex.h | 81 + lib/lua/src/llimits.h | 128 + lib/lua/src/lmathlib.c | 263 + lib/lua/src/lmem.c | 86 + lib/lua/src/lmem.h | 49 + lib/lua/src/loadlib.c | 666 + lib/lua/src/lobject.c | 214 + lib/lua/src/lobject.h | 381 + lib/lua/src/lopcodes.c | 102 + lib/lua/src/lopcodes.h | 268 + lib/lua/src/loslib.c | 243 + lib/lua/src/lparser.c | 1339 ++ lib/lua/src/lparser.h | 82 + lib/lua/src/lstate.c | 214 + lib/lua/src/lstate.h | 169 + lib/lua/src/lstring.c | 111 + lib/lua/src/lstring.h | 31 + lib/lua/src/lstrlib.c | 871 + lib/lua/src/ltable.c | 588 + lib/lua/src/ltable.h | 40 + lib/lua/src/ltablib.c | 287 + lib/lua/src/ltm.c | 75 + lib/lua/src/ltm.h | 54 + lib/lua/src/lua.c | 392 + lib/lua/src/lua.h | 388 + lib/lua/src/luac.c | 200 + lib/lua/src/luaconf.h | 771 + lib/lua/src/lualib.h | 53 + lib/lua/src/lundump.c | 227 + lib/lua/src/lundump.h | 36 + lib/lua/src/lvm.c | 767 + lib/lua/src/lvm.h | 36 + lib/lua/src/lzio.c | 82 + lib/lua/src/lzio.h | 67 + lib/lua/src/print.c | 227 + minetest.conf.example | 3524 +++ misc/CPACK_WIX_UI_BANNER.BMP | Bin 0 -> 114514 bytes misc/CPACK_WIX_UI_DIALOG.BMP | Bin 0 -> 461882 bytes misc/Info.plist | 20 + misc/debpkg-control | 33 + misc/irrlichtmt_tag.txt | 1 + misc/kubernetes.yml | 53 + misc/minetest-icon-24x24.png | Bin 0 -> 587 bytes misc/minetest-icon.icns | Bin 0 -> 242668 bytes misc/minetest-icon.ico | Bin 0 -> 9662 bytes misc/minetest-xorg-icon-128.png | Bin 0 -> 11241 bytes misc/minetest.exe.manifest | 17 + misc/minetest.svg | 183 + misc/net.minetest.minetest.appdata.xml | 87 + misc/net.minetest.minetest.desktop | 18 + misc/winresource.rc | 66 + po/ar/minetest.po | 7139 ++++++ po/be/minetest.po | 8320 +++++++ po/bg/minetest.po | 6986 ++++++ po/ca/minetest.po | 7564 +++++++ po/cs/minetest.po | 7879 +++++++ po/da/minetest.po | 7933 +++++++ po/de/minetest.po | 8473 ++++++++ po/dv/minetest.po | 6986 ++++++ po/el/minetest.po | 6965 ++++++ po/eo/minetest.po | 8297 +++++++ po/es/minetest.po | 8194 +++++++ po/et/minetest.po | 7096 ++++++ po/eu/minetest.po | 7008 ++++++ po/fi/minetest.po | 6950 ++++++ po/fil/minetest.po | 7064 ++++++ po/fr/minetest.po | 8412 ++++++++ po/gd/minetest.po | 7199 +++++++ po/gl/minetest.po | 8021 +++++++ po/he/minetest.po | 7122 ++++++ po/hi/minetest.po | 7083 ++++++ po/hu/minetest.po | 8332 +++++++ po/id/minetest.po | 8293 +++++++ po/it/minetest.po | 8484 ++++++++ po/ja/minetest.po | 8223 +++++++ po/jbo/minetest.po | 7098 ++++++ po/kk/minetest.po | 6872 ++++++ po/kn/minetest.po | 6897 ++++++ po/ko/minetest.po | 7739 +++++++ po/ky/minetest.po | 7167 ++++++ po/lt/minetest.po | 7199 +++++++ po/lv/minetest.po | 7083 ++++++ po/lzh/minetest.po | 6842 ++++++ po/minetest.pot | 6840 ++++++ po/mr/minetest.po | 6865 ++++++ po/ms/minetest.po | 8377 +++++++ po/ms_Arab/minetest.po | 7824 +++++++ po/nb/minetest.po | 7513 +++++++ po/nl/minetest.po | 8424 ++++++++ po/nn/minetest.po | 7092 ++++++ po/oc/minetest.po | 6859 ++++++ po/pl/minetest.po | 8645 ++++++++ po/pt/minetest.po | 8365 +++++++ po/pt_BR/minetest.po | 8434 ++++++++ po/ro/minetest.po | 7233 +++++++ po/ru/minetest.po | 8523 ++++++++ po/sk/minetest.po | 8151 +++++++ po/sl/minetest.po | 7323 +++++++ po/sr_Cyrl/minetest.po | 7256 +++++++ po/sr_Latn/minetest.po | 6875 ++++++ po/sv/minetest.po | 7332 +++++++ po/sw/minetest.po | 8189 +++++++ po/th/minetest.po | 8085 +++++++ po/tr/minetest.po | 8353 +++++++ po/tt/minetest.po | 6846 ++++++ po/uk/minetest.po | 7234 +++++++ po/vi/minetest.po | 6983 ++++++ po/yue/minetest.po | 6839 ++++++ po/zh_CN/minetest.po | 8079 +++++++ po/zh_TW/minetest.po | 8007 +++++++ src/CMakeLists.txt | 910 + src/activeobject.h | 132 + src/activeobjectmgr.h | 66 + src/ban.cpp | 142 + src/ban.h | 49 + src/benchmark/CMakeLists.txt | 7 + src/benchmark/benchmark.cpp | 32 + src/benchmark/benchmark.h | 26 + src/benchmark/benchmark_serialize.cpp | 71 + src/benchmark/benchmark_setup.h | 22 + src/chat.cpp | 845 + src/chat.h | 309 + src/chat_interface.h | 79 + src/chatmessage.h | 49 + src/client/CMakeLists.txt | 66 + src/client/activeobjectmgr.cpp | 109 + src/client/activeobjectmgr.h | 41 + src/client/camera.cpp | 708 + src/client/camera.h | 270 + src/client/client.cpp | 2065 ++ src/client/client.h | 609 + src/client/clientenvironment.cpp | 515 + src/client/clientenvironment.h | 156 + src/client/clientevent.h | 148 + src/client/clientlauncher.cpp | 662 + src/client/clientlauncher.h | 55 + src/client/clientmap.cpp | 964 + src/client/clientmap.h | 209 + src/client/clientmedia.cpp | 792 + src/client/clientmedia.h | 250 + src/client/clientobject.cpp | 66 + src/client/clientobject.h | 116 + src/client/clientsimpleobject.h | 35 + src/client/clouds.cpp | 385 + src/client/clouds.h | 143 + src/client/content_cao.cpp | 1988 ++ src/client/content_cao.h | 282 + src/client/content_cso.cpp | 77 + src/client/content_cso.h | 26 + src/client/content_mapblock.cpp | 1635 ++ src/client/content_mapblock.h | 180 + src/client/event_manager.h | 86 + src/client/filecache.cpp | 97 + src/client/filecache.h | 43 + src/client/fontengine.cpp | 270 + src/client/fontengine.h | 170 + src/client/game.cpp | 4376 ++++ src/client/game.h | 53 + src/client/gameui.cpp | 331 + src/client/gameui.h | 138 + src/client/guiscalingfilter.cpp | 240 + src/client/guiscalingfilter.h | 58 + src/client/hud.cpp | 1260 ++ src/client/hud.h | 173 + src/client/imagefilters.cpp | 249 + src/client/imagefilters.h | 43 + src/client/inputhandler.cpp | 278 + src/client/inputhandler.h | 435 + src/client/joystick_controller.cpp | 330 + src/client/joystick_controller.h | 172 + src/client/keycode.cpp | 386 + src/client/keycode.h | 67 + src/client/keys.h | 120 + src/client/localplayer.cpp | 1138 + src/client/localplayer.h | 215 + src/client/mapblock_mesh.cpp | 1581 ++ src/client/mapblock_mesh.h | 332 + src/client/mesh.cpp | 499 + src/client/mesh.h | 135 + src/client/mesh_generator_thread.cpp | 321 + src/client/mesh_generator_thread.h | 135 + src/client/meshgen/collector.cpp | 104 + src/client/meshgen/collector.h | 65 + src/client/minimap.cpp | 762 + src/client/minimap.h | 176 + src/client/particles.cpp | 927 + src/client/particles.h | 242 + src/client/render/anaglyph.cpp | 52 + src/client/render/anaglyph.h | 34 + src/client/render/core.cpp | 129 + src/client/render/core.h | 80 + src/client/render/factory.cpp | 52 + src/client/render/factory.h | 27 + src/client/render/interlaced.cpp | 120 + src/client/render/interlaced.h | 43 + src/client/render/pageflip.cpp | 59 + src/client/render/pageflip.h | 43 + src/client/render/plain.cpp | 76 + src/client/render/plain.h | 38 + src/client/render/sidebyside.cpp | 74 + src/client/render/sidebyside.h | 43 + src/client/render/stereo.cpp | 60 + src/client/render/stereo.h | 38 + src/client/renderingengine.cpp | 652 + src/client/renderingengine.h | 138 + src/client/shader.cpp | 829 + src/client/shader.h | 156 + src/client/shadows/dynamicshadows.cpp | 191 + src/client/shadows/dynamicshadows.h | 120 + src/client/shadows/dynamicshadowsrender.cpp | 709 + src/client/shadows/dynamicshadowsrender.h | 162 + src/client/shadows/shadowsScreenQuad.cpp | 61 + src/client/shadows/shadowsScreenQuad.h | 54 + src/client/shadows/shadowsshadercallbacks.cpp | 48 + src/client/shadows/shadowsshadercallbacks.h | 58 + src/client/sky.cpp | 898 + src/client/sky.h | 216 + src/client/sound.cpp | 23 + src/client/sound.h | 93 + src/client/sound_openal.cpp | 734 + src/client/sound_openal.h | 31 + src/client/tile.cpp | 2333 ++ src/client/tile.h | 341 + src/client/wieldmesh.cpp | 765 + src/client/wieldmesh.h | 146 + src/clientiface.cpp | 863 + src/clientiface.h | 536 + src/cmake_config.h.in | 40 + src/cmake_config_githash.h.in | 6 + src/collision.cpp | 613 + src/collision.h | 88 + src/config.h | 27 + src/constants.h | 113 + src/content/CMakeLists.txt | 7 + src/content/content.cpp | 122 + src/content/content.h | 42 + src/content/mod_configuration.cpp | 255 + src/content/mod_configuration.h | 111 + src/content/mods.cpp | 245 + src/content/mods.h | 129 + src/content/subgames.cpp | 423 + src/content/subgames.h | 101 + src/content_mapnode.cpp | 167 + src/content_mapnode.h | 34 + src/content_nodemeta.cpp | 191 + src/content_nodemeta.h | 34 + src/convert_json.cpp | 39 + src/convert_json.h | 27 + src/craftdef.cpp | 1124 + src/craftdef.h | 463 + src/database/CMakeLists.txt | 10 + src/database/database-dummy.cpp | 120 + src/database/database-dummy.h | 53 + src/database/database-files.cpp | 501 + src/database/database-files.h | 97 + src/database/database-leveldb.cpp | 309 + src/database/database-leveldb.h | 80 + src/database/database-postgresql.cpp | 816 + src/database/database-postgresql.h | 171 + src/database/database-redis.cpp | 201 + src/database/database-redis.h | 51 + src/database/database-sqlite3.cpp | 869 + src/database/database-sqlite3.h | 259 + src/database/database.cpp | 69 + src/database/database.h | 99 + src/daynightratio.h | 72 + src/debug.cpp | 197 + src/debug.h | 102 + src/defaultsettings.cpp | 518 + src/defaultsettings.h | 28 + src/emerge.cpp | 753 + src/emerge.h | 236 + src/environment.cpp | 324 + src/environment.h | 156 + src/exceptions.h | 126 + src/face_position_cache.cpp | 110 + src/face_position_cache.h | 41 + src/filesys.cpp | 879 + src/filesys.h | 153 + src/gamedef.h | 85 + src/gameparams.h | 58 + src/gettext.cpp | 255 + src/gettext.h | 107 + src/gettime.h | 64 + src/gui/CMakeLists.txt | 34 + src/gui/StyleSpec.h | 479 + src/gui/guiAnimatedImage.cpp | 62 + src/gui/guiAnimatedImage.h | 41 + src/gui/guiBackgroundImage.cpp | 63 + src/gui/guiBackgroundImage.h | 38 + src/gui/guiBox.cpp | 117 + src/gui/guiBox.h | 41 + src/gui/guiButton.cpp | 766 + src/gui/guiButton.h | 346 + src/gui/guiButtonImage.cpp | 94 + src/gui/guiButtonImage.h | 51 + src/gui/guiButtonItemImage.cpp | 59 + src/gui/guiButtonItemImage.h | 47 + src/gui/guiChatConsole.cpp | 728 + src/gui/guiChatConsole.h | 137 + src/gui/guiEditBox.cpp | 848 + src/gui/guiEditBox.h | 215 + src/gui/guiEditBoxWithScrollbar.cpp | 660 + src/gui/guiEditBoxWithScrollbar.h | 63 + src/gui/guiEngine.cpp | 635 + src/gui/guiEngine.h | 304 + src/gui/guiFormSpecMenu.cpp | 4759 ++++ src/gui/guiFormSpecMenu.h | 499 + src/gui/guiHyperText.cpp | 1153 + src/gui/guiHyperText.h | 230 + src/gui/guiInventoryList.cpp | 235 + src/gui/guiInventoryList.h | 133 + src/gui/guiItemImage.cpp | 52 + src/gui/guiItemImage.h | 46 + src/gui/guiKeyChangeMenu.cpp | 438 + src/gui/guiKeyChangeMenu.h | 79 + src/gui/guiMainMenu.h | 57 + src/gui/guiPasswordChange.cpp | 303 + src/gui/guiPasswordChange.h | 61 + src/gui/guiPathSelectMenu.cpp | 112 + src/gui/guiPathSelectMenu.h | 63 + src/gui/guiScene.cpp | 274 + src/gui/guiScene.h | 87 + src/gui/guiScrollBar.cpp | 446 + src/gui/guiScrollBar.h | 77 + src/gui/guiScrollContainer.cpp | 81 + src/gui/guiScrollContainer.h | 62 + src/gui/guiSkin.cpp | 1042 + src/gui/guiSkin.h | 366 + src/gui/guiTable.cpp | 1289 ++ src/gui/guiTable.h | 263 + src/gui/guiVolumeChange.cpp | 182 + src/gui/guiVolumeChange.h | 51 + src/gui/mainmenumanager.h | 148 + src/gui/modalMenu.cpp | 361 + src/gui/modalMenu.h | 111 + src/gui/profilergraph.cpp | 176 + src/gui/profilergraph.h | 61 + src/gui/touchscreengui.cpp | 1200 ++ src/gui/touchscreengui.h | 314 + src/httpfetch.cpp | 848 + src/httpfetch.h | 133 + src/hud.cpp | 68 + src/hud.h | 126 + src/inventory.cpp | 961 + src/inventory.h | 353 + src/inventorymanager.cpp | 999 + src/inventorymanager.h | 258 + src/irr_aabb3d.h | 26 + src/irr_ptr.h | 200 + src/irr_v2d.h | 30 + src/irr_v3d.h | 30 + src/irrlicht_changes/CGUITTFont.cpp | 1244 ++ src/irrlicht_changes/CGUITTFont.h | 412 + src/irrlicht_changes/CMakeLists.txt | 11 + src/irrlicht_changes/static_text.cpp | 589 + src/irrlicht_changes/static_text.h | 237 + src/irrlichttypes.h | 62 + src/irrlichttypes_bloated.h | 28 + src/irrlichttypes_extrabloated.h | 36 + src/itemdef.cpp | 600 + src/itemdef.h | 186 + src/itemgroup.h | 33 + src/itemstackmetadata.cpp | 118 + src/itemstackmetadata.h | 54 + src/light.cpp | 93 + src/light.h | 82 + src/lighting.h | 27 + src/log.cpp | 395 + src/log.h | 375 + src/main.cpp | 1130 + src/map.cpp | 2062 ++ src/map.h | 483 + src/map_settings_manager.cpp | 180 + src/map_settings_manager.h | 77 + src/mapblock.cpp | 915 + src/mapblock.h | 640 + src/mapgen/CMakeLists.txt | 19 + src/mapgen/cavegen.cpp | 915 + src/mapgen/cavegen.h | 248 + src/mapgen/dungeongen.cpp | 674 + src/mapgen/dungeongen.h | 126 + src/mapgen/mapgen.cpp | 1105 + src/mapgen/mapgen.h | 331 + src/mapgen/mapgen_carpathian.cpp | 572 + src/mapgen/mapgen_carpathian.h | 114 + src/mapgen/mapgen_flat.cpp | 335 + src/mapgen/mapgen_flat.h | 90 + src/mapgen/mapgen_fractal.cpp | 448 + src/mapgen/mapgen_fractal.h | 98 + src/mapgen/mapgen_singlenode.cpp | 93 + src/mapgen/mapgen_singlenode.h | 48 + src/mapgen/mapgen_v5.cpp | 325 + src/mapgen/mapgen_v5.h | 80 + src/mapgen/mapgen_v6.cpp | 1117 + src/mapgen/mapgen_v6.h | 173 + src/mapgen/mapgen_v7.cpp | 590 + src/mapgen/mapgen_v7.h | 122 + src/mapgen/mapgen_valleys.cpp | 483 + src/mapgen/mapgen_valleys.h | 110 + src/mapgen/mg_biome.cpp | 331 + src/mapgen/mg_biome.h | 252 + src/mapgen/mg_decoration.cpp | 467 + src/mapgen/mg_decoration.h | 152 + src/mapgen/mg_ore.cpp | 600 + src/mapgen/mg_ore.h | 201 + src/mapgen/mg_schematic.cpp | 636 + src/mapgen/mg_schematic.h | 156 + src/mapgen/treegen.cpp | 875 + src/mapgen/treegen.h | 91 + src/mapnode.cpp | 863 + src/mapnode.h | 307 + src/mapsector.cpp | 130 + src/mapsector.h | 87 + src/metadata.cpp | 115 + src/metadata.h | 63 + src/modchannels.cpp | 151 + src/modchannels.h | 93 + src/modifiedstate.h | 33 + src/mtevent.h | 71 + src/nameidmapping.cpp | 48 + src/nameidmapping.h | 91 + src/network/CMakeLists.txt | 18 + src/network/address.cpp | 259 + src/network/address.h | 82 + src/network/clientopcodes.cpp | 226 + src/network/clientopcodes.h | 50 + src/network/clientpackethandler.cpp | 1767 ++ src/network/connection.cpp | 1647 ++ src/network/connection.h | 785 + src/network/connectionthreads.cpp | 1358 ++ src/network/connectionthreads.h | 169 + src/network/networkexceptions.h | 100 + src/network/networkpacket.cpp | 559 + src/network/networkpacket.h | 137 + src/network/networkprotocol.h | 1141 + src/network/peerhandler.h | 77 + src/network/serveropcodes.cpp | 225 + src/network/serveropcodes.h | 50 + src/network/serverpackethandler.cpp | 1843 ++ src/network/socket.cpp | 372 + src/network/socket.h | 56 + src/nodedef.cpp | 1844 ++ src/nodedef.h | 867 + src/nodemetadata.cpp | 258 + src/nodemetadata.h | 117 + src/nodetimer.cpp | 150 + src/nodetimer.h | 128 + src/noise.cpp | 733 + src/noise.h | 232 + src/objdef.cpp | 203 + src/objdef.h | 113 + src/object_properties.cpp | 240 + src/object_properties.h | 75 + src/particles.cpp | 176 + src/particles.h | 430 + src/pathfinder.cpp | 1441 ++ src/pathfinder.h | 64 + src/player.cpp | 236 + src/player.h | 224 + src/porting.cpp | 791 + src/porting.h | 359 + src/porting_android.cpp | 305 + src/porting_android.h | 86 + src/profiler.cpp | 182 + src/profiler.h | 109 + src/raycast.cpp | 137 + src/raycast.h | 77 + src/reflowscan.cpp | 205 + src/reflowscan.h | 47 + src/remoteplayer.cpp | 117 + src/remoteplayer.h | 171 + src/rollback.cpp | 966 + src/rollback.h | 103 + src/rollback_interface.cpp | 236 + src/rollback_interface.h | 157 + src/script/CMakeLists.txt | 21 + src/script/common/CMakeLists.txt | 12 + src/script/common/c_content.cpp | 2180 ++ src/script/common/c_content.h | 209 + src/script/common/c_converter.cpp | 674 + src/script/common/c_converter.h | 123 + src/script/common/c_internal.cpp | 182 + src/script/common/c_internal.h | 146 + src/script/common/c_packer.cpp | 596 + src/script/common/c_packer.h | 126 + src/script/common/c_types.cpp | 34 + src/script/common/c_types.h | 61 + src/script/common/helper.cpp | 88 + src/script/common/helper.h | 55 + src/script/cpp_api/CMakeLists.txt | 20 + src/script/cpp_api/s_async.cpp | 389 + src/script/cpp_api/s_async.h | 197 + src/script/cpp_api/s_base.cpp | 465 + src/script/cpp_api/s_base.h | 180 + src/script/cpp_api/s_client.cpp | 298 + src/script/cpp_api/s_client.h | 64 + src/script/cpp_api/s_entity.cpp | 334 + src/script/cpp_api/s_entity.h | 54 + src/script/cpp_api/s_env.cpp | 290 + src/script/cpp_api/s_env.h | 50 + src/script/cpp_api/s_internal.h | 85 + src/script/cpp_api/s_inventory.cpp | 225 + src/script/cpp_api/s_inventory.h | 59 + src/script/cpp_api/s_item.cpp | 275 + src/script/cpp_api/s_item.h | 70 + src/script/cpp_api/s_mainmenu.cpp | 93 + src/script/cpp_api/s_mainmenu.h | 45 + src/script/cpp_api/s_modchannels.cpp | 50 + src/script/cpp_api/s_modchannels.h | 31 + src/script/cpp_api/s_node.cpp | 286 + src/script/cpp_api/s_node.h | 59 + src/script/cpp_api/s_nodemeta.cpp | 232 + src/script/cpp_api/s_nodemeta.h | 63 + src/script/cpp_api/s_player.cpp | 382 + src/script/cpp_api/s_player.h | 89 + src/script/cpp_api/s_security.cpp | 901 + src/script/cpp_api/s_security.h | 83 + src/script/cpp_api/s_server.cpp | 262 + src/script/cpp_api/s_server.h | 61 + src/script/lua_api/CMakeLists.txt | 37 + src/script/lua_api/l_areastore.cpp | 397 + src/script/lua_api/l_areastore.h | 64 + src/script/lua_api/l_auth.cpp | 219 + src/script/lua_api/l_auth.h | 54 + src/script/lua_api/l_base.cpp | 139 + src/script/lua_api/l_base.h | 91 + src/script/lua_api/l_camera.cpp | 241 + src/script/lua_api/l_camera.h | 60 + src/script/lua_api/l_client.cpp | 446 + src/script/lua_api/l_client.h | 110 + src/script/lua_api/l_craft.cpp | 535 + src/script/lua_api/l_craft.h | 49 + src/script/lua_api/l_env.cpp | 1512 ++ src/script/lua_api/l_env.h | 340 + src/script/lua_api/l_http.cpp | 249 + src/script/lua_api/l_http.h | 58 + src/script/lua_api/l_internal.h | 79 + src/script/lua_api/l_inventory.cpp | 543 + src/script/lua_api/l_inventory.h | 127 + src/script/lua_api/l_item.cpp | 714 + src/script/lua_api/l_item.h | 174 + src/script/lua_api/l_itemstackmeta.cpp | 149 + src/script/lua_api/l_itemstackmeta.h | 72 + src/script/lua_api/l_localplayer.cpp | 495 + src/script/lua_api/l_localplayer.h | 113 + src/script/lua_api/l_mainmenu.cpp | 1081 + src/script/lua_api/l_mainmenu.h | 174 + src/script/lua_api/l_mapgen.cpp | 1808 ++ src/script/lua_api/l_mapgen.h | 149 + src/script/lua_api/l_metadata.cpp | 303 + src/script/lua_api/l_metadata.h | 81 + src/script/lua_api/l_minimap.cpp | 231 + src/script/lua_api/l_minimap.h | 62 + src/script/lua_api/l_modchannels.cpp | 153 + src/script/lua_api/l_modchannels.h | 66 + src/script/lua_api/l_nodemeta.cpp | 280 + src/script/lua_api/l_nodemeta.h | 97 + src/script/lua_api/l_nodetimer.cpp | 141 + src/script/lua_api/l_nodetimer.h | 61 + src/script/lua_api/l_noise.cpp | 763 + src/script/lua_api/l_noise.h | 201 + src/script/lua_api/l_object.cpp | 2503 +++ src/script/lua_api/l_object.h | 388 + src/script/lua_api/l_particleparams.h | 282 + src/script/lua_api/l_particles.cpp | 327 + src/script/lua_api/l_particles.h | 32 + src/script/lua_api/l_particles_local.cpp | 225 + src/script/lua_api/l_particles_local.h | 34 + src/script/lua_api/l_playermeta.cpp | 123 + src/script/lua_api/l_playermeta.h | 57 + src/script/lua_api/l_rollback.cpp | 117 + src/script/lua_api/l_rollback.h | 35 + src/script/lua_api/l_server.cpp | 646 + src/script/lua_api/l_server.h | 121 + src/script/lua_api/l_settings.cpp | 396 + src/script/lua_api/l_settings.h | 88 + src/script/lua_api/l_sound.cpp | 53 + src/script/lua_api/l_sound.h | 33 + src/script/lua_api/l_storage.cpp | 155 + src/script/lua_api/l_storage.h | 60 + src/script/lua_api/l_util.cpp | 710 + src/script/lua_api/l_util.h | 135 + src/script/lua_api/l_vmanip.cpp | 512 + src/script/lua_api/l_vmanip.h | 82 + src/script/scripting_client.cpp | 100 + src/script/scripting_client.h | 47 + src/script/scripting_mainmenu.cpp | 100 + src/script/scripting_mainmenu.h | 49 + src/script/scripting_server.cpp | 191 + src/script/scripting_server.h | 70 + src/serialization.cpp | 402 + src/serialization.h | 101 + src/server.cpp | 4147 ++++ src/server.h | 731 + src/server/CMakeLists.txt | 9 + src/server/activeobjectmgr.cpp | 184 + src/server/activeobjectmgr.h | 49 + src/server/luaentity_sao.cpp | 560 + src/server/luaentity_sao.h | 107 + src/server/mods.cpp | 108 + src/server/mods.h | 73 + src/server/player_sao.cpp | 679 + src/server/player_sao.h | 306 + src/server/serveractiveobject.cpp | 91 + src/server/serveractiveobject.h | 274 + src/server/serverinventorymgr.cpp | 210 + src/server/serverinventorymgr.h | 61 + src/server/unit_sao.cpp | 369 + src/server/unit_sao.h | 137 + src/serverenvironment.cpp | 2436 +++ src/serverenvironment.h | 506 + src/serverlist.cpp | 109 + src/serverlist.h | 39 + src/settings.cpp | 1101 + src/settings.h | 298 + src/settings_translation_file.cpp | 1161 + src/skyparams.h | 160 + src/sound.h | 69 + src/staticobject.cpp | 123 + src/staticobject.h | 93 + src/terminal_chat_console.cpp | 457 + src/terminal_chat_console.h | 124 + src/texture_override.cpp | 134 + src/texture_override.h | 84 + src/threading/CMakeLists.txt | 6 + src/threading/event.cpp | 44 + src/threading/event.h | 46 + src/threading/mutex_auto_lock.h | 30 + src/threading/semaphore.cpp | 167 + src/threading/semaphore.h | 52 + src/threading/thread.cpp | 347 + src/threading/thread.h | 160 + src/tileanimation.cpp | 116 + src/tileanimation.h | 59 + src/tool.cpp | 383 + src/tool.h | 146 + src/translation.cpp | 162 + src/translation.h | 40 + src/unittest/CMakeLists.txt | 55 + src/unittest/test.cpp | 678 + src/unittest/test.h | 142 + src/unittest/test_activeobject.cpp | 60 + src/unittest/test_address.cpp | 67 + src/unittest/test_areastore.cpp | 173 + src/unittest/test_authdatabase.cpp | 296 + src/unittest/test_ban.cpp | 170 + src/unittest/test_clientactiveobjectmgr.cpp | 118 + src/unittest/test_collision.cpp | 180 + src/unittest/test_compression.cpp | 273 + src/unittest/test_config.h.in | 7 + src/unittest/test_connection.cpp | 378 + src/unittest/test_eventmanager.cpp | 112 + src/unittest/test_filepath.cpp | 264 + src/unittest/test_gameui.cpp | 93 + src/unittest/test_gettext.cpp | 43 + src/unittest/test_inventory.cpp | 126 + src/unittest/test_irrptr.cpp | 143 + src/unittest/test_keycode.cpp | 129 + src/unittest/test_lua.cpp | 79 + src/unittest/test_map.cpp | 68 + src/unittest/test_map_settings_manager.cpp | 266 + src/unittest/test_mapnode.cpp | 57 + src/unittest/test_mod/test_mod/init.lua | 1 + src/unittest/test_mod/test_mod/mod.conf | 2 + src/unittest/test_modchannels.cpp | 76 + src/unittest/test_modmetadatadatabase.cpp | 253 + src/unittest/test_nodedef.cpp | 67 + src/unittest/test_noderesolver.cpp | 212 + src/unittest/test_noise.cpp | 340 + src/unittest/test_objdef.cpp | 174 + src/unittest/test_profiler.cpp | 73 + src/unittest/test_random.cpp | 275 + src/unittest/test_schematic.cpp | 286 + src/unittest/test_serialization.cpp | 439 + src/unittest/test_server_shutdown_state.cpp | 120 + src/unittest/test_serveractiveobjectmgr.cpp | 200 + src/unittest/test_servermodmanager.cpp | 201 + src/unittest/test_settings.cpp | 296 + src/unittest/test_socket.cpp | 149 + src/unittest/test_threading.cpp | 158 + src/unittest/test_utilities.cpp | 638 + src/unittest/test_voxelalgorithms.cpp | 101 + src/unittest/test_voxelarea.cpp | 388 + src/unittest/test_voxelmanipulator.cpp | 108 + src/unittest/test_world/do_not_remove.txt | 0 src/util/CMakeLists.txt | 19 + src/util/Optional.h | 105 + src/util/areastore.cpp | 329 + src/util/areastore.h | 197 + src/util/auth.cpp | 137 + src/util/auth.h | 47 + src/util/base64.cpp | 154 + src/util/base64.h | 26 + src/util/basic_macros.h | 64 + src/util/container.h | 307 + src/util/directiontables.cpp | 120 + src/util/directiontables.h | 89 + src/util/enriched_string.cpp | 211 + src/util/enriched_string.h | 110 + src/util/hex.h | 60 + src/util/ieee_float.cpp | 136 + src/util/ieee_float.h | 34 + src/util/md32_common.h | 430 + src/util/metricsbackend.cpp | 204 + src/util/metricsbackend.h | 72 + src/util/numeric.cpp | 223 + src/util/numeric.h | 471 + src/util/png.cpp | 68 + src/util/png.h | 27 + src/util/pointedthing.cpp | 134 + src/util/pointedthing.h | 105 + src/util/pointer.h | 273 + src/util/quicktune.cpp | 104 + src/util/quicktune.h | 98 + src/util/quicktune_shortcutter.h | 84 + src/util/serialize.cpp | 294 + src/util/serialize.h | 475 + src/util/sha1.cpp | 198 + src/util/sha1.h | 54 + src/util/sha2.h | 154 + src/util/sha256.c | 399 + src/util/srp.cpp | 1040 + src/util/srp.h | 191 + src/util/stream.h | 70 + src/util/strfnd.h | 78 + src/util/string.cpp | 908 + src/util/string.h | 763 + src/util/thread.h | 228 + src/util/timetaker.cpp | 63 + src/util/timetaker.h | 50 + src/version.cpp | 45 + src/version.h | 24 + src/voxel.cpp | 320 + src/voxel.h | 508 + src/voxelalgorithms.cpp | 1332 ++ src/voxelalgorithms.h | 157 + textures/base/pack/air.png | Bin 0 -> 225 bytes textures/base/pack/aux1_btn.png | Bin 0 -> 1652 bytes textures/base/pack/blank.png | Bin 0 -> 95 bytes textures/base/pack/bubble.png | Bin 0 -> 142 bytes textures/base/pack/bubble_gone.png | Bin 0 -> 68 bytes textures/base/pack/camera_btn.png | Bin 0 -> 1859 bytes textures/base/pack/cdb_add.png | Bin 0 -> 147 bytes textures/base/pack/cdb_clear.png | Bin 0 -> 182 bytes textures/base/pack/cdb_downloading.png | Bin 0 -> 201 bytes textures/base/pack/cdb_queued.png | Bin 0 -> 210 bytes textures/base/pack/cdb_update.png | Bin 0 -> 173 bytes textures/base/pack/cdb_viewonline.png | Bin 0 -> 191 bytes textures/base/pack/chat_btn.png | Bin 0 -> 873 bytes textures/base/pack/chat_hide_btn.png | Bin 0 -> 1089 bytes textures/base/pack/chat_show_btn.png | Bin 0 -> 1082 bytes textures/base/pack/checkbox_16.png | Bin 0 -> 288 bytes textures/base/pack/checkbox_16_white.png | Bin 0 -> 173 bytes textures/base/pack/checkbox_32.png | Bin 0 -> 436 bytes textures/base/pack/checkbox_64.png | Bin 0 -> 766 bytes textures/base/pack/clear.png | Bin 0 -> 708 bytes textures/base/pack/crack_anylength.png | Bin 0 -> 255 bytes textures/base/pack/debug_btn.png | Bin 0 -> 2484 bytes textures/base/pack/down.png | Bin 0 -> 1690 bytes textures/base/pack/drop_btn.png | Bin 0 -> 1269 bytes textures/base/pack/end_icon.png | Bin 0 -> 908 bytes textures/base/pack/error_icon_orange.png | Bin 0 -> 133 bytes textures/base/pack/error_icon_red.png | Bin 0 -> 133 bytes textures/base/pack/error_screenshot.png | Bin 0 -> 971 bytes textures/base/pack/fast_btn.png | Bin 0 -> 1212 bytes textures/base/pack/fly_btn.png | Bin 0 -> 1559 bytes textures/base/pack/gear_icon.png | Bin 0 -> 1858 bytes textures/base/pack/halo.png | Bin 0 -> 144 bytes textures/base/pack/heart.png | Bin 0 -> 255 bytes textures/base/pack/heart_gone.png | Bin 0 -> 68 bytes textures/base/pack/ignore.png | Bin 0 -> 234 bytes textures/base/pack/inventory_btn.png | Bin 0 -> 331 bytes textures/base/pack/joystick_bg.png | Bin 0 -> 12481 bytes textures/base/pack/joystick_center.png | Bin 0 -> 2574 bytes textures/base/pack/joystick_off.png | Bin 0 -> 14363 bytes textures/base/pack/jump_btn.png | Bin 0 -> 1710 bytes textures/base/pack/loading_screenshot.png | Bin 0 -> 580 bytes textures/base/pack/logo.png | Bin 0 -> 12188 bytes textures/base/pack/menu_bg.png | Bin 0 -> 124 bytes textures/base/pack/menu_header.png | Bin 0 -> 1628 bytes textures/base/pack/minimap_btn.png | Bin 0 -> 1220 bytes textures/base/pack/minimap_mask_round.png | Bin 0 -> 1858 bytes textures/base/pack/minimap_mask_square.png | Bin 0 -> 420 bytes textures/base/pack/minimap_overlay_round.png | Bin 0 -> 22044 bytes textures/base/pack/minimap_overlay_square.png | Bin 0 -> 1686 bytes textures/base/pack/next_icon.png | Bin 0 -> 714 bytes textures/base/pack/no_screenshot.png | Bin 0 -> 2043 bytes textures/base/pack/no_texture.png | Bin 0 -> 281 bytes textures/base/pack/no_texture_airlike.png | Bin 0 -> 178 bytes textures/base/pack/noclip_btn.png | Bin 0 -> 1236 bytes textures/base/pack/object_marker_red.png | Bin 0 -> 449 bytes textures/base/pack/player.png | Bin 0 -> 142 bytes textures/base/pack/player_back.png | Bin 0 -> 140 bytes textures/base/pack/player_marker.png | Bin 0 -> 2166 bytes textures/base/pack/plus.png | Bin 0 -> 763 bytes textures/base/pack/prev_icon.png | Bin 0 -> 714 bytes textures/base/pack/progress_bar.png | Bin 0 -> 413 bytes textures/base/pack/progress_bar_bg.png | Bin 0 -> 354 bytes textures/base/pack/rangeview_btn.png | Bin 0 -> 2423 bytes textures/base/pack/rare_controls.png | Bin 0 -> 227 bytes textures/base/pack/refresh.png | Bin 0 -> 3660 bytes textures/base/pack/search.png | Bin 0 -> 1908 bytes textures/base/pack/server_favorite.png | Bin 0 -> 916 bytes textures/base/pack/server_favorite_delete.png | Bin 0 -> 748 bytes textures/base/pack/server_flags_creative.png | Bin 0 -> 273 bytes textures/base/pack/server_flags_damage.png | Bin 0 -> 713 bytes textures/base/pack/server_flags_pvp.png | Bin 0 -> 1048 bytes textures/base/pack/server_incompatible.png | Bin 0 -> 385 bytes textures/base/pack/server_ping_1.png | Bin 0 -> 251 bytes textures/base/pack/server_ping_2.png | Bin 0 -> 244 bytes textures/base/pack/server_ping_3.png | Bin 0 -> 245 bytes textures/base/pack/server_ping_4.png | Bin 0 -> 213 bytes textures/base/pack/server_public.png | Bin 0 -> 492 bytes textures/base/pack/smoke_puff.png | Bin 0 -> 202 bytes textures/base/pack/start_icon.png | Bin 0 -> 912 bytes textures/base/pack/sunrisebg.png | Bin 0 -> 4435 bytes textures/base/pack/unknown_item.png | Bin 0 -> 292 bytes textures/base/pack/unknown_node.png | Bin 0 -> 193 bytes textures/base/pack/unknown_object.png | Bin 0 -> 254 bytes textures/base/pack/wieldhand.png | Bin 0 -> 126 bytes textures/base/pack/zoom.png | Bin 0 -> 1320 bytes util/buildbot/buildwin32.sh | 190 + util/buildbot/buildwin64.sh | 187 + .../toolchain_i686-w64-mingw32-posix.cmake | 19 + .../buildbot/toolchain_i686-w64-mingw32.cmake | 17 + .../toolchain_x86_64-w64-mingw32-posix.cmake | 19 + .../toolchain_x86_64-w64-mingw32.cmake | 17 + util/bump_version.sh | 144 + util/ci/build.sh | 10 + util/ci/build_prometheus_cpp.sh | 13 + util/ci/clang-format-whitelist.txt | 500 + util/ci/clang-format.sh | 64 + util/ci/clang-tidy.sh | 13 + util/ci/common.sh | 34 + util/ci/run-clang-tidy.py | 321 + util/fix_format.sh | 5 + util/gather_git_credits.py | 67 + util/helper_mod/init.lua | 51 + util/helper_mod/mod.conf | 3 + util/reorder_translation_commits.py | 33 + util/stress_mapgen.sh | 30 + util/test_multiplayer.sh | 57 + util/updatepo.sh | 82 + util/wireshark/minetest.lua | 1415 ++ 1522 files changed, 753891 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SECURITY.md create mode 100644 .github/workflows/android.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/cpp_lint.yml create mode 100644 .github/workflows/lua.yml create mode 100644 .github/workflows/macos.yml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .luacheckrc create mode 100644 .mailmap create mode 100644 AppImageBuilder.yml create mode 100644 CMakeLists.txt create mode 100644 Dockerfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/net/minetest/minetest/CustomEditText.java create mode 100644 android/app/src/main/java/net/minetest/minetest/GameActivity.java create mode 100644 android/app/src/main/java/net/minetest/minetest/MainActivity.java create mode 100644 android/app/src/main/java/net/minetest/minetest/UnzipService.java create mode 100644 android/app/src/main/java/net/minetest/minetest/Utils.java create mode 100644 android/app/src/main/res/drawable/background.png create mode 100644 android/app/src/main/res/drawable/bg.xml create mode 100644 android/app/src/main/res/layout/activity_main.xml create mode 100644 android/app/src/main/res/mipmap/ic_launcher.png create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/main/res/xml/filepaths.xml create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/icons/aux1_btn.svg create mode 100644 android/icons/camera_btn.svg create mode 100644 android/icons/chat_btn.svg create mode 100644 android/icons/chat_hide_btn.svg create mode 100644 android/icons/chat_show_btn.svg create mode 100644 android/icons/checkbox_tick.svg create mode 100644 android/icons/debug_btn.svg create mode 100644 android/icons/down.svg create mode 100644 android/icons/drop_btn.svg create mode 100644 android/icons/fast_btn.svg create mode 100644 android/icons/fly_btn.svg create mode 100644 android/icons/gear_icon.svg create mode 100644 android/icons/inventory_btn.svg create mode 100644 android/icons/joystick_bg.svg create mode 100644 android/icons/joystick_center.svg create mode 100644 android/icons/joystick_off.svg create mode 100644 android/icons/jump_btn.svg create mode 100644 android/icons/minimap_btn.svg create mode 100644 android/icons/noclip_btn.svg create mode 100644 android/icons/rangeview_btn.svg create mode 100644 android/icons/rare_controls.svg create mode 100644 android/icons/zoom.svg create mode 100644 android/keystore-minetest.jks create mode 100644 android/native/build.gradle create mode 100644 android/native/jni/Android.mk create mode 100644 android/native/jni/Application.mk create mode 100644 android/native/src/main/AndroidManifest.xml create mode 100644 android/settings.gradle create mode 100644 builtin/async/game.lua create mode 100644 builtin/async/mainmenu.lua create mode 100644 builtin/client/chatcommands.lua create mode 100644 builtin/client/death_formspec.lua create mode 100644 builtin/client/init.lua create mode 100644 builtin/client/misc.lua create mode 100644 builtin/client/register.lua create mode 100644 builtin/common/after.lua create mode 100644 builtin/common/chatcommands.lua create mode 100644 builtin/common/filterlist.lua create mode 100644 builtin/common/information_formspecs.lua create mode 100644 builtin/common/misc_helpers.lua create mode 100644 builtin/common/mod_storage.lua create mode 100644 builtin/common/serialize.lua create mode 100644 builtin/common/strict.lua create mode 100644 builtin/common/tests/misc_helpers_spec.lua create mode 100644 builtin/common/tests/serialize_spec.lua create mode 100644 builtin/common/tests/vector_spec.lua create mode 100644 builtin/common/vector.lua create mode 100644 builtin/fstk/buttonbar.lua create mode 100644 builtin/fstk/dialog.lua create mode 100644 builtin/fstk/tabview.lua create mode 100644 builtin/fstk/ui.lua create mode 100644 builtin/game/async.lua create mode 100644 builtin/game/auth.lua create mode 100644 builtin/game/chat.lua create mode 100644 builtin/game/constants.lua create mode 100644 builtin/game/deprecated.lua create mode 100644 builtin/game/detached_inventory.lua create mode 100644 builtin/game/falling.lua create mode 100644 builtin/game/features.lua create mode 100644 builtin/game/forceloading.lua create mode 100644 builtin/game/init.lua create mode 100644 builtin/game/item.lua create mode 100644 builtin/game/item_entity.lua create mode 100644 builtin/game/item_s.lua create mode 100644 builtin/game/knockback.lua create mode 100644 builtin/game/misc.lua create mode 100644 builtin/game/misc_s.lua create mode 100644 builtin/game/privileges.lua create mode 100644 builtin/game/register.lua create mode 100644 builtin/game/statbars.lua create mode 100644 builtin/game/static_spawn.lua create mode 100644 builtin/game/voxelarea.lua create mode 100644 builtin/init.lua create mode 100644 builtin/locale/__builtin.de.tr create mode 100644 builtin/locale/__builtin.it.tr create mode 100644 builtin/locale/template.txt create mode 100644 builtin/mainmenu/async_event.lua create mode 100644 builtin/mainmenu/common.lua create mode 100644 builtin/mainmenu/dlg_config_world.lua create mode 100644 builtin/mainmenu/dlg_contentstore.lua create mode 100644 builtin/mainmenu/dlg_create_world.lua create mode 100644 builtin/mainmenu/dlg_delete_content.lua create mode 100644 builtin/mainmenu/dlg_delete_world.lua create mode 100644 builtin/mainmenu/dlg_register.lua create mode 100644 builtin/mainmenu/dlg_rename_modpack.lua create mode 100644 builtin/mainmenu/dlg_settings_advanced.lua create mode 100644 builtin/mainmenu/dlg_version_info.lua create mode 100644 builtin/mainmenu/game_theme.lua create mode 100644 builtin/mainmenu/generate_from_settingtypes.lua create mode 100644 builtin/mainmenu/init.lua create mode 100644 builtin/mainmenu/pkgmgr.lua create mode 100644 builtin/mainmenu/serverlistmgr.lua create mode 100644 builtin/mainmenu/tab_about.lua create mode 100644 builtin/mainmenu/tab_content.lua create mode 100644 builtin/mainmenu/tab_local.lua create mode 100644 builtin/mainmenu/tab_online.lua create mode 100644 builtin/mainmenu/tab_settings.lua create mode 100644 builtin/mainmenu/tests/favorites_wellformed.txt create mode 100644 builtin/mainmenu/tests/serverlistmgr_spec.lua create mode 100644 builtin/profiler/init.lua create mode 100644 builtin/profiler/instrumentation.lua create mode 100644 builtin/profiler/reporter.lua create mode 100644 builtin/profiler/sampling.lua create mode 100644 builtin/settingtypes.txt create mode 100644 client/serverlist/.gitignore create mode 100644 client/shaders/3d_interlaced_merge/opengl_fragment.glsl create mode 100644 client/shaders/3d_interlaced_merge/opengl_vertex.glsl create mode 100644 client/shaders/default_shader/opengl_fragment.glsl create mode 100644 client/shaders/default_shader/opengl_vertex.glsl create mode 100644 client/shaders/minimap_shader/opengl_fragment.glsl create mode 100644 client/shaders/minimap_shader/opengl_vertex.glsl create mode 100644 client/shaders/nodes_shader/opengl_fragment.glsl create mode 100644 client/shaders/nodes_shader/opengl_vertex.glsl create mode 100644 client/shaders/object_shader/opengl_fragment.glsl create mode 100644 client/shaders/object_shader/opengl_vertex.glsl create mode 100644 client/shaders/selection_shader/opengl_fragment.glsl create mode 100644 client/shaders/selection_shader/opengl_vertex.glsl create mode 100644 client/shaders/shadow_shaders/pass1_fragment.glsl create mode 100644 client/shaders/shadow_shaders/pass1_trans_fragment.glsl create mode 100644 client/shaders/shadow_shaders/pass1_trans_vertex.glsl create mode 100644 client/shaders/shadow_shaders/pass1_vertex.glsl create mode 100644 client/shaders/shadow_shaders/pass2_fragment.glsl create mode 100644 client/shaders/shadow_shaders/pass2_vertex.glsl create mode 100644 client/shaders/stars_shader/opengl_fragment.glsl create mode 100644 client/shaders/stars_shader/opengl_vertex.glsl create mode 100644 clientmods/preview/example.lua create mode 100644 clientmods/preview/examples/first.lua create mode 100644 clientmods/preview/init.lua create mode 100644 clientmods/preview/mod.conf create mode 100644 clientmods/preview/settingtypes.txt create mode 100644 cmake/Modules/FindCURL.cmake create mode 100644 cmake/Modules/FindGMP.cmake create mode 100644 cmake/Modules/FindGettextLib.cmake create mode 100644 cmake/Modules/FindJson.cmake create mode 100644 cmake/Modules/FindLua.cmake create mode 100644 cmake/Modules/FindLuaJIT.cmake create mode 100644 cmake/Modules/FindNcursesw.cmake create mode 100644 cmake/Modules/FindSQLite3.cmake create mode 100644 cmake/Modules/FindVorbis.cmake create mode 100644 cmake/Modules/FindZstd.cmake create mode 100644 cmake/Modules/GenerateVersion.cmake create mode 100644 cmake/Modules/MinetestFindIrrlichtHeaders.cmake create mode 100644 doc/Doxyfile.in create mode 100644 doc/README.android create mode 100644 doc/breakages.md create mode 100644 doc/builtin_entities.txt create mode 100644 doc/client_lua_api.txt create mode 100644 doc/direction.md create mode 100644 doc/fst_api.txt create mode 100644 doc/lgpl-2.1.txt create mode 100644 doc/lua_api.txt create mode 100644 doc/main_page.dox create mode 100644 doc/menu_lua_api.txt create mode 100644 doc/minetest.6 create mode 100644 doc/minetestserver.6 create mode 100644 doc/mkdocs/build.sh create mode 100644 doc/mkdocs/docs/css/code_styles.css create mode 100644 doc/mkdocs/docs/css/extra.css create mode 120000 doc/mkdocs/docs/img/favicon.ico create mode 100644 doc/mkdocs/lua_highlight.patch create mode 100644 doc/mod_channels.png create mode 100644 doc/protocol.txt create mode 100644 doc/texture_packs.txt create mode 100644 doc/world_format.txt create mode 100644 fonts/Arimo-Bold.ttf create mode 100644 fonts/Arimo-BoldItalic.ttf create mode 100644 fonts/Arimo-Italic.ttf create mode 100644 fonts/Arimo-LICENSE.txt create mode 100644 fonts/Arimo-Regular.ttf create mode 100644 fonts/Cousine-Bold.ttf create mode 100644 fonts/Cousine-BoldItalic.ttf create mode 100644 fonts/Cousine-Italic.ttf create mode 100644 fonts/Cousine-LICENSE.txt create mode 100644 fonts/Cousine-Regular.ttf create mode 100644 fonts/DroidSansFallbackFull-LICENSE.txt create mode 100644 fonts/DroidSansFallbackFull.ttf create mode 100644 games/devtest/.luacheckrc create mode 100644 games/devtest/LICENSE.txt create mode 100644 games/devtest/README.md create mode 100644 games/devtest/game.conf create mode 100644 games/devtest/menu/background.png create mode 100644 games/devtest/menu/header.png create mode 100644 games/devtest/menu/icon.png create mode 100644 games/devtest/mods/basenodes/init.lua create mode 100644 games/devtest/mods/basenodes/mod.conf create mode 100644 games/devtest/mods/basenodes/textures/basenodes_dirt_with_grass_bottom.png create mode 100644 games/devtest/mods/basenodes/textures/basenodes_dirt_with_snow.png create mode 100644 games/devtest/mods/basenodes/textures/basenodes_dirt_with_snow_bottom.png create mode 100644 games/devtest/mods/basenodes/textures/basenodes_snow_sheet.png create mode 100644 games/devtest/mods/basenodes/textures/default_apple.png create mode 100644 games/devtest/mods/basenodes/textures/default_cobble.png create mode 100644 games/devtest/mods/basenodes/textures/default_desert_sand.png create mode 100644 games/devtest/mods/basenodes/textures/default_desert_stone.png create mode 100644 games/devtest/mods/basenodes/textures/default_dirt.png create mode 100644 games/devtest/mods/basenodes/textures/default_grass.png create mode 100644 games/devtest/mods/basenodes/textures/default_gravel.png create mode 100644 games/devtest/mods/basenodes/textures/default_ice.png create mode 100644 games/devtest/mods/basenodes/textures/default_junglegrass.png create mode 100644 games/devtest/mods/basenodes/textures/default_jungleleaves.png create mode 100644 games/devtest/mods/basenodes/textures/default_jungletree.png create mode 100644 games/devtest/mods/basenodes/textures/default_jungletree_top.png create mode 100644 games/devtest/mods/basenodes/textures/default_lava.png create mode 100644 games/devtest/mods/basenodes/textures/default_lava_flowing.png create mode 100644 games/devtest/mods/basenodes/textures/default_leaves.png create mode 100644 games/devtest/mods/basenodes/textures/default_mossycobble.png create mode 100644 games/devtest/mods/basenodes/textures/default_pine_needles.png create mode 100644 games/devtest/mods/basenodes/textures/default_pine_tree.png create mode 100644 games/devtest/mods/basenodes/textures/default_pine_tree_top.png create mode 100644 games/devtest/mods/basenodes/textures/default_river_water.png create mode 100644 games/devtest/mods/basenodes/textures/default_river_water_flowing.png create mode 100644 games/devtest/mods/basenodes/textures/default_sand.png create mode 100644 games/devtest/mods/basenodes/textures/default_snow.png create mode 100644 games/devtest/mods/basenodes/textures/default_snow_side.png create mode 100644 games/devtest/mods/basenodes/textures/default_stone.png create mode 100644 games/devtest/mods/basenodes/textures/default_tree.png create mode 100644 games/devtest/mods/basenodes/textures/default_tree_top.png create mode 100644 games/devtest/mods/basenodes/textures/default_water.png create mode 100644 games/devtest/mods/basenodes/textures/default_water_flowing.png create mode 100644 games/devtest/mods/basenodes/textures/dirt_with_grass/default_grass.png create mode 100644 games/devtest/mods/basenodes/textures/dirt_with_grass/default_grass_side.png create mode 100644 games/devtest/mods/basenodes/textures/info.txt create mode 100644 games/devtest/mods/basetools/init.lua create mode 100644 games/devtest/mods/basetools/mod.conf create mode 100644 games/devtest/mods/basetools/textures/basetools_bloodsword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_elementalsword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_firesword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_healdagger.png create mode 100644 games/devtest/mods/basetools/textures/basetools_healsword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_icesword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_mesepick.png create mode 100644 games/devtest/mods/basetools/textures/basetools_mesesword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_steelaxe.png create mode 100644 games/devtest/mods/basetools/textures/basetools_steeldagger.png create mode 100644 games/devtest/mods/basetools/textures/basetools_steelpick.png create mode 100644 games/devtest/mods/basetools/textures/basetools_steelpick_l1.png create mode 100644 games/devtest/mods/basetools/textures/basetools_steelpick_l2.png create mode 100644 games/devtest/mods/basetools/textures/basetools_steelshears.png create mode 100644 games/devtest/mods/basetools/textures/basetools_steelshovel.png create mode 100644 games/devtest/mods/basetools/textures/basetools_steelsword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_stoneaxe.png create mode 100644 games/devtest/mods/basetools/textures/basetools_stonepick.png create mode 100644 games/devtest/mods/basetools/textures/basetools_stoneshears.png create mode 100644 games/devtest/mods/basetools/textures/basetools_stoneshovel.png create mode 100644 games/devtest/mods/basetools/textures/basetools_stonesword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_superhealsword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_titaniumsword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_usespick.png create mode 100644 games/devtest/mods/basetools/textures/basetools_usessword.png create mode 100644 games/devtest/mods/basetools/textures/basetools_woodaxe.png create mode 100644 games/devtest/mods/basetools/textures/basetools_wooddagger.png create mode 100644 games/devtest/mods/basetools/textures/basetools_woodpick.png create mode 100644 games/devtest/mods/basetools/textures/basetools_woodshears.png create mode 100644 games/devtest/mods/basetools/textures/basetools_woodshovel.png create mode 100644 games/devtest/mods/basetools/textures/basetools_woodsword.png create mode 100644 games/devtest/mods/broken/init.lua create mode 100644 games/devtest/mods/broken/mod.conf create mode 100644 games/devtest/mods/bucket/init.lua create mode 100644 games/devtest/mods/bucket/mod.conf create mode 100644 games/devtest/mods/bucket/textures/bucket.png create mode 100644 games/devtest/mods/bucket/textures/bucket_lava.png create mode 100644 games/devtest/mods/bucket/textures/bucket_water.png create mode 100644 games/devtest/mods/chest/init.lua create mode 100644 games/devtest/mods/chest/mod.conf create mode 100644 games/devtest/mods/chest/textures/chest_chest.png create mode 100644 games/devtest/mods/chest_of_everything/init.lua create mode 100644 games/devtest/mods/chest_of_everything/mod.conf create mode 100644 games/devtest/mods/chest_of_everything/textures/chest_of_everything_chest.png create mode 100644 games/devtest/mods/dignodes/init.lua create mode 100644 games/devtest/mods/dignodes/mod.conf create mode 100644 games/devtest/mods/dignodes/textures/dignodes_choppy.png create mode 100644 games/devtest/mods/dignodes/textures/dignodes_cracky.png create mode 100644 games/devtest/mods/dignodes/textures/dignodes_crumbly.png create mode 100644 games/devtest/mods/dignodes/textures/dignodes_dig_immediate.png create mode 100644 games/devtest/mods/dignodes/textures/dignodes_none.png create mode 100644 games/devtest/mods/dignodes/textures/dignodes_rating1.png create mode 100644 games/devtest/mods/dignodes/textures/dignodes_rating2.png create mode 100644 games/devtest/mods/dignodes/textures/dignodes_rating3.png create mode 100644 games/devtest/mods/experimental/commands.lua create mode 100644 games/devtest/mods/experimental/detached.lua create mode 100644 games/devtest/mods/experimental/init.lua create mode 100644 games/devtest/mods/experimental/items.lua create mode 100644 games/devtest/mods/experimental/mod.conf create mode 100644 games/devtest/mods/experimental/textures/experimental_callback_node.png create mode 100644 games/devtest/mods/experimental/textures/experimental_particle_sheet.png create mode 100644 games/devtest/mods/experimental/textures/experimental_particle_vertical.png create mode 100644 games/devtest/mods/experimental/textures/experimental_tester_tool_1.png create mode 100644 games/devtest/mods/give_initial_stuff/init.lua create mode 100644 games/devtest/mods/give_initial_stuff/mod.conf create mode 100644 games/devtest/mods/initial_message/init.lua create mode 100644 games/devtest/mods/initial_message/mod.conf create mode 100644 games/devtest/mods/mapgen/init.lua create mode 100644 games/devtest/mods/mapgen/mod.conf create mode 100644 games/devtest/mods/modchannels/init.lua create mode 100644 games/devtest/mods/modchannels/mod.conf create mode 100644 games/devtest/mods/soundstuff/init.lua create mode 100644 games/devtest/mods/soundstuff/mod.conf create mode 100644 games/devtest/mods/soundstuff/sounds/soundstuff_mono.ogg create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_eat.png create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_node_blank.png create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_node_climbable.png create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_node_dig.png create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_node_dug.png create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_node_fall.png create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_node_footstep.png create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_node_place.png create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_node_place_failed.png create mode 100644 games/devtest/mods/soundstuff/textures/soundstuff_node_sound.png create mode 100644 games/devtest/mods/stairs/init.lua create mode 100644 games/devtest/mods/stairs/mod.conf create mode 100644 games/devtest/mods/testentities/armor.lua create mode 100644 games/devtest/mods/testentities/callbacks.lua create mode 100644 games/devtest/mods/testentities/init.lua create mode 100644 games/devtest/mods/testentities/mod.conf create mode 100644 games/devtest/mods/testentities/textures/testentities_armorball.png create mode 100644 games/devtest/mods/testentities/textures/testentities_callback.png create mode 100644 games/devtest/mods/testentities/textures/testentities_callback_step.png create mode 100644 games/devtest/mods/testentities/textures/testentities_cube1.png create mode 100644 games/devtest/mods/testentities/textures/testentities_cube2.png create mode 100644 games/devtest/mods/testentities/textures/testentities_cube3.png create mode 100644 games/devtest/mods/testentities/textures/testentities_cube4.png create mode 100644 games/devtest/mods/testentities/textures/testentities_cube5.png create mode 100644 games/devtest/mods/testentities/textures/testentities_cube6.png create mode 100644 games/devtest/mods/testentities/textures/testentities_dungeon_master.png create mode 100644 games/devtest/mods/testentities/textures/testentities_sprite.png create mode 100644 games/devtest/mods/testentities/textures/testentities_upright_sprite1.png create mode 100644 games/devtest/mods/testentities/textures/testentities_upright_sprite2.png create mode 100644 games/devtest/mods/testentities/visuals.lua create mode 100644 games/devtest/mods/testfood/init.lua create mode 100644 games/devtest/mods/testfood/mod.conf create mode 100644 games/devtest/mods/testfood/textures/testfood_bad.png create mode 100644 games/devtest/mods/testfood/textures/testfood_bad2.png create mode 100644 games/devtest/mods/testfood/textures/testfood_good.png create mode 100644 games/devtest/mods/testfood/textures/testfood_good2.png create mode 100644 games/devtest/mods/testfood/textures/testfood_replace.png create mode 100644 games/devtest/mods/testformspec/LICENSE.txt create mode 100644 games/devtest/mods/testformspec/callbacks.lua create mode 100644 games/devtest/mods/testformspec/dummy_items.lua create mode 100644 games/devtest/mods/testformspec/formspec.lua create mode 100644 games/devtest/mods/testformspec/init.lua create mode 100644 games/devtest/mods/testformspec/mod.conf create mode 100644 games/devtest/mods/testformspec/models/testformspec_character.b3d create mode 100644 games/devtest/mods/testformspec/models/testformspec_chest.obj create mode 100644 games/devtest/mods/testformspec/textures/default_chest_front.png create mode 100644 games/devtest/mods/testformspec/textures/default_chest_inside.png create mode 100644 games/devtest/mods/testformspec/textures/default_chest_side.png create mode 100644 games/devtest/mods/testformspec/textures/default_chest_top.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_9slice.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_animation.jpg create mode 100644 games/devtest/mods/testformspec/textures/testformspec_animation.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_bg.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_bg_9slice.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_bg_9slice_hovered.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_bg_9slice_pressed.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_bg_hovered.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_bg_pressed.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_button_image.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_character.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_hovered.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_item.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_node.png create mode 100644 games/devtest/mods/testformspec/textures/testformspec_pressed.png create mode 100644 games/devtest/mods/testhud/init.lua create mode 100644 games/devtest/mods/testhud/mod.conf create mode 100644 games/devtest/mods/testitems/init.lua create mode 100644 games/devtest/mods/testitems/mod.conf create mode 100644 games/devtest/mods/testitems/textures/testitems_overlay_base.png create mode 100644 games/devtest/mods/testitems/textures/testitems_overlay_overlay.png create mode 100644 games/devtest/mods/testnodes/README.md create mode 100644 games/devtest/mods/testnodes/drawtypes.lua create mode 100644 games/devtest/mods/testnodes/init.lua create mode 100644 games/devtest/mods/testnodes/light.lua create mode 100644 games/devtest/mods/testnodes/liquids.lua create mode 100644 games/devtest/mods/testnodes/meshes.lua create mode 100644 games/devtest/mods/testnodes/mod.conf create mode 100644 games/devtest/mods/testnodes/models/testnodes_ocorner.obj create mode 100644 games/devtest/mods/testnodes/models/testnodes_pyramid.obj create mode 100644 games/devtest/mods/testnodes/nodeboxes.lua create mode 100644 games/devtest/mods/testnodes/overlays.lua create mode 100644 games/devtest/mods/testnodes/param2.lua create mode 100644 games/devtest/mods/testnodes/properties.lua create mode 100644 games/devtest/mods/testnodes/textures.lua create mode 100644 games/devtest/mods/testnodes/textures/testnodes_1.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_1g.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_1w.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_1wg.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_2.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_2g.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_2w.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_2wg.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_3.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_3g.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_3w.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_3wg.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_4.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_4g.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_4w.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_4wg.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_5.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_5g.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_5w.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_5wg.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_6.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_6g.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_6w.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_6wg.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_airlike.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_allfaces.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_allfaces_optional.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_alpha.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_alpha128.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_alpha191.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_alpha64.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_anim.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_attached_bottom.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_attached_side.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_attached_top.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_attachedw_bottom.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_attachedw_side.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_attachedw_top.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_bouncy.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_buildable_to.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_climbable_nojump_side.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_climbable_resistance_side.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_climbable_side.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_damage.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_damage_neg.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_drowning.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_fall_damage_minus.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_fall_damage_plus.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_fencelike.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_firelike.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_glasslike.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_glasslike_detail.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_glasslike_framed.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_glasslike_framed2.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_glasslike_framed_optional.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_glasslikeliquid.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_1.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_10.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_11.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_12.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_13.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_14.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_2.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_3.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_4.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_5.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_6.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_7.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_8.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_light_9.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_line_crossing.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_line_curved.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_line_straight.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_line_t_junction.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquid.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing_r0.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing_r1.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing_r2.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing_r3.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing_r4.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing_r5.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing_r6.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing_r7.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidflowing_r8.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource_r0.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource_r1.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource_r2.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource_r3.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource_r4.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource_r5.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource_r6.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource_r7.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_liquidsource_r8.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_mesh_stripes.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_mesh_stripes2.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_mesh_stripes3.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_mesh_stripes4.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_move_resistance.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_node.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_node_falling.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_nodebox.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_nojump_side.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_nojump_top.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_normal.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_normal1.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_normal2.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_normal3.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_normal4.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_normal5.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_normal6.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_overlay.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_overlayable.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_palette_facedir.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_palette_full.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_palette_wallmounted.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_degrotate.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_leveled.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_meshoptions.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_base.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_base_side_degrotate.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_base_side_leveled.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_base_side_meshoptions.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_base_side_wallmounted.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_base_side_waving.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_degrotate.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_leveled.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_meshoptions.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_wallmounted.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_rooted_waving.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_wallmounted.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_plantlike_waving.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_rail2_crossing.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_rail2_curved.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_rail2_straight.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_rail2_t_junction.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_rail_crossing.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_rail_curved.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_rail_straight.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_rail_t_junction.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_signlike.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_slippery.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_street_crossing.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_street_curved.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_street_straight.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_street_t_junction.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_sunlight_filter.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type10_32bpp_bt.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type10_32bpp_tb.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type1_24bpp_bt.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type1_24bpp_tb.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type2_16bpp_bt.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type2_16bpp_tb.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type2_32bpp_bt.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type2_32bpp_tb.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type3_16bpp_bt.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_tga_type3_16bpp_tb.tga create mode 100644 games/devtest/mods/testnodes/textures/testnodes_torchlike_ceiling.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_torchlike_floor.png create mode 100644 games/devtest/mods/testnodes/textures/testnodes_torchlike_wall.png create mode 100644 games/devtest/mods/testpathfinder/README.md create mode 100644 games/devtest/mods/testpathfinder/init.lua create mode 100644 games/devtest/mods/testpathfinder/mod.conf create mode 100644 games/devtest/mods/testpathfinder/textures/testpathfinder_testpathfinder.png create mode 100644 games/devtest/mods/testpathfinder/textures/testpathfinder_waypoint.png create mode 100644 games/devtest/mods/testpathfinder/textures/testpathfinder_waypoint_end.png create mode 100644 games/devtest/mods/testpathfinder/textures/testpathfinder_waypoint_start.png create mode 100644 games/devtest/mods/testtools/README.md create mode 100644 games/devtest/mods/testtools/init.lua create mode 100644 games/devtest/mods/testtools/light.lua create mode 100644 games/devtest/mods/testtools/mod.conf create mode 100644 games/devtest/mods/testtools/textures/testtools_children_getter.png create mode 100644 games/devtest/mods/testtools/textures/testtools_entity_rotator.png create mode 100644 games/devtest/mods/testtools/textures/testtools_entity_scaler.png create mode 100644 games/devtest/mods/testtools/textures/testtools_entity_spawner.png create mode 100644 games/devtest/mods/testtools/textures/testtools_falling_node_tool.png create mode 100644 games/devtest/mods/testtools/textures/testtools_item_meta_editor.png create mode 100644 games/devtest/mods/testtools/textures/testtools_lighttool.png create mode 100644 games/devtest/mods/testtools/textures/testtools_node_meta_editor.png create mode 100644 games/devtest/mods/testtools/textures/testtools_node_setter.png create mode 100644 games/devtest/mods/testtools/textures/testtools_object_attacher.png create mode 100644 games/devtest/mods/testtools/textures/testtools_object_editor.png create mode 100644 games/devtest/mods/testtools/textures/testtools_object_mover.png create mode 100644 games/devtest/mods/testtools/textures/testtools_param2tool.png create mode 100644 games/devtest/mods/testtools/textures/testtools_remover.png create mode 100644 games/devtest/mods/tiled/init.lua create mode 100644 games/devtest/mods/tiled/mod.conf create mode 100644 games/devtest/mods/tiled/textures/tiled_tiled.png create mode 100644 games/devtest/mods/unittests/async_env.lua create mode 100644 games/devtest/mods/unittests/crafting.lua create mode 100644 games/devtest/mods/unittests/crafting_prepare.lua create mode 100644 games/devtest/mods/unittests/entity.lua create mode 100644 games/devtest/mods/unittests/init.lua create mode 100644 games/devtest/mods/unittests/inside_async_env.lua create mode 100644 games/devtest/mods/unittests/itemdescription.lua create mode 100644 games/devtest/mods/unittests/misc.lua create mode 100644 games/devtest/mods/unittests/mod.conf create mode 100644 games/devtest/mods/unittests/player.lua create mode 100644 games/devtest/mods/unittests/textures/default_dirt.png create mode 100644 games/devtest/mods/unittests/textures/unittests_coal_lump.png create mode 100644 games/devtest/mods/unittests/textures/unittests_description_test.png create mode 100644 games/devtest/mods/unittests/textures/unittests_iron_lump.png create mode 100644 games/devtest/mods/unittests/textures/unittests_repairable_tool.png create mode 100644 games/devtest/mods/unittests/textures/unittests_steel_ingot.png create mode 100644 games/devtest/mods/unittests/textures/unittests_stick.png create mode 100644 games/devtest/mods/unittests/textures/unittests_torch.png create mode 100644 games/devtest/mods/unittests/textures/unittests_unrepairable_tool.png create mode 100644 games/devtest/mods/util_commands/init.lua create mode 100644 games/devtest/mods/util_commands/mod.conf create mode 100644 games/devtest/screenshot.png create mode 100644 games/devtest/settingtypes.txt create mode 100644 lib/bitop/CMakeLists.txt create mode 100644 lib/bitop/bit.c create mode 100644 lib/bitop/bit.h create mode 100644 lib/catch2/CMakeLists.txt create mode 100644 lib/catch2/catch.hpp create mode 100644 lib/gmp/CMakeLists.txt create mode 100644 lib/gmp/mini-gmp.c create mode 100644 lib/gmp/mini-gmp.h create mode 100644 lib/jsoncpp/CMakeLists.txt create mode 100644 lib/jsoncpp/json/UPDATING create mode 100644 lib/jsoncpp/json/json-forwards.h create mode 100644 lib/jsoncpp/json/json.h create mode 100644 lib/jsoncpp/jsoncpp.cpp create mode 100644 lib/lua/CMakeLists.txt create mode 100644 lib/lua/COPYRIGHT create mode 100644 lib/lua/src/CMakeLists.txt create mode 100644 lib/lua/src/lapi.c create mode 100644 lib/lua/src/lapi.h create mode 100644 lib/lua/src/lauxlib.c create mode 100644 lib/lua/src/lauxlib.h create mode 100644 lib/lua/src/lbaselib.c create mode 100644 lib/lua/src/lcode.c create mode 100644 lib/lua/src/lcode.h create mode 100644 lib/lua/src/ldblib.c create mode 100644 lib/lua/src/ldebug.c create mode 100644 lib/lua/src/ldebug.h create mode 100644 lib/lua/src/ldo.c create mode 100644 lib/lua/src/ldo.h create mode 100644 lib/lua/src/ldump.c create mode 100644 lib/lua/src/lfunc.c create mode 100644 lib/lua/src/lfunc.h create mode 100644 lib/lua/src/lgc.c create mode 100644 lib/lua/src/lgc.h create mode 100644 lib/lua/src/linit.c create mode 100644 lib/lua/src/liolib.c create mode 100644 lib/lua/src/llex.c create mode 100644 lib/lua/src/llex.h create mode 100644 lib/lua/src/llimits.h create mode 100644 lib/lua/src/lmathlib.c create mode 100644 lib/lua/src/lmem.c create mode 100644 lib/lua/src/lmem.h create mode 100644 lib/lua/src/loadlib.c create mode 100644 lib/lua/src/lobject.c create mode 100644 lib/lua/src/lobject.h create mode 100644 lib/lua/src/lopcodes.c create mode 100644 lib/lua/src/lopcodes.h create mode 100644 lib/lua/src/loslib.c create mode 100644 lib/lua/src/lparser.c create mode 100644 lib/lua/src/lparser.h create mode 100644 lib/lua/src/lstate.c create mode 100644 lib/lua/src/lstate.h create mode 100644 lib/lua/src/lstring.c create mode 100644 lib/lua/src/lstring.h create mode 100644 lib/lua/src/lstrlib.c create mode 100644 lib/lua/src/ltable.c create mode 100644 lib/lua/src/ltable.h create mode 100644 lib/lua/src/ltablib.c create mode 100644 lib/lua/src/ltm.c create mode 100644 lib/lua/src/ltm.h create mode 100644 lib/lua/src/lua.c create mode 100644 lib/lua/src/lua.h create mode 100644 lib/lua/src/luac.c create mode 100644 lib/lua/src/luaconf.h create mode 100644 lib/lua/src/lualib.h create mode 100644 lib/lua/src/lundump.c create mode 100644 lib/lua/src/lundump.h create mode 100644 lib/lua/src/lvm.c create mode 100644 lib/lua/src/lvm.h create mode 100644 lib/lua/src/lzio.c create mode 100644 lib/lua/src/lzio.h create mode 100644 lib/lua/src/print.c create mode 100644 minetest.conf.example create mode 100644 misc/CPACK_WIX_UI_BANNER.BMP create mode 100644 misc/CPACK_WIX_UI_DIALOG.BMP create mode 100644 misc/Info.plist create mode 100644 misc/debpkg-control create mode 100644 misc/irrlichtmt_tag.txt create mode 100644 misc/kubernetes.yml create mode 100644 misc/minetest-icon-24x24.png create mode 100644 misc/minetest-icon.icns create mode 100644 misc/minetest-icon.ico create mode 100644 misc/minetest-xorg-icon-128.png create mode 100644 misc/minetest.exe.manifest create mode 100644 misc/minetest.svg create mode 100644 misc/net.minetest.minetest.appdata.xml create mode 100644 misc/net.minetest.minetest.desktop create mode 100644 misc/winresource.rc create mode 100644 po/ar/minetest.po create mode 100644 po/be/minetest.po create mode 100644 po/bg/minetest.po create mode 100644 po/ca/minetest.po create mode 100644 po/cs/minetest.po create mode 100644 po/da/minetest.po create mode 100644 po/de/minetest.po create mode 100644 po/dv/minetest.po create mode 100644 po/el/minetest.po create mode 100644 po/eo/minetest.po create mode 100644 po/es/minetest.po create mode 100644 po/et/minetest.po create mode 100644 po/eu/minetest.po create mode 100644 po/fi/minetest.po create mode 100644 po/fil/minetest.po create mode 100644 po/fr/minetest.po create mode 100644 po/gd/minetest.po create mode 100644 po/gl/minetest.po create mode 100644 po/he/minetest.po create mode 100644 po/hi/minetest.po create mode 100644 po/hu/minetest.po create mode 100644 po/id/minetest.po create mode 100644 po/it/minetest.po create mode 100644 po/ja/minetest.po create mode 100644 po/jbo/minetest.po create mode 100644 po/kk/minetest.po create mode 100644 po/kn/minetest.po create mode 100644 po/ko/minetest.po create mode 100644 po/ky/minetest.po create mode 100644 po/lt/minetest.po create mode 100644 po/lv/minetest.po create mode 100644 po/lzh/minetest.po create mode 100644 po/minetest.pot create mode 100644 po/mr/minetest.po create mode 100644 po/ms/minetest.po create mode 100644 po/ms_Arab/minetest.po create mode 100644 po/nb/minetest.po create mode 100644 po/nl/minetest.po create mode 100644 po/nn/minetest.po create mode 100644 po/oc/minetest.po create mode 100644 po/pl/minetest.po create mode 100644 po/pt/minetest.po create mode 100644 po/pt_BR/minetest.po create mode 100644 po/ro/minetest.po create mode 100644 po/ru/minetest.po create mode 100644 po/sk/minetest.po create mode 100644 po/sl/minetest.po create mode 100644 po/sr_Cyrl/minetest.po create mode 100644 po/sr_Latn/minetest.po create mode 100644 po/sv/minetest.po create mode 100644 po/sw/minetest.po create mode 100644 po/th/minetest.po create mode 100644 po/tr/minetest.po create mode 100644 po/tt/minetest.po create mode 100644 po/uk/minetest.po create mode 100644 po/vi/minetest.po create mode 100644 po/yue/minetest.po create mode 100644 po/zh_CN/minetest.po create mode 100644 po/zh_TW/minetest.po create mode 100644 src/CMakeLists.txt create mode 100644 src/activeobject.h create mode 100644 src/activeobjectmgr.h create mode 100644 src/ban.cpp create mode 100644 src/ban.h create mode 100644 src/benchmark/CMakeLists.txt create mode 100644 src/benchmark/benchmark.cpp create mode 100644 src/benchmark/benchmark.h create mode 100644 src/benchmark/benchmark_serialize.cpp create mode 100644 src/benchmark/benchmark_setup.h create mode 100644 src/chat.cpp create mode 100644 src/chat.h create mode 100644 src/chat_interface.h create mode 100644 src/chatmessage.h create mode 100644 src/client/CMakeLists.txt create mode 100644 src/client/activeobjectmgr.cpp create mode 100644 src/client/activeobjectmgr.h create mode 100644 src/client/camera.cpp create mode 100644 src/client/camera.h create mode 100644 src/client/client.cpp create mode 100644 src/client/client.h create mode 100644 src/client/clientenvironment.cpp create mode 100644 src/client/clientenvironment.h create mode 100644 src/client/clientevent.h create mode 100644 src/client/clientlauncher.cpp create mode 100644 src/client/clientlauncher.h create mode 100644 src/client/clientmap.cpp create mode 100644 src/client/clientmap.h create mode 100644 src/client/clientmedia.cpp create mode 100644 src/client/clientmedia.h create mode 100644 src/client/clientobject.cpp create mode 100644 src/client/clientobject.h create mode 100644 src/client/clientsimpleobject.h create mode 100644 src/client/clouds.cpp create mode 100644 src/client/clouds.h create mode 100644 src/client/content_cao.cpp create mode 100644 src/client/content_cao.h create mode 100644 src/client/content_cso.cpp create mode 100644 src/client/content_cso.h create mode 100644 src/client/content_mapblock.cpp create mode 100644 src/client/content_mapblock.h create mode 100644 src/client/event_manager.h create mode 100644 src/client/filecache.cpp create mode 100644 src/client/filecache.h create mode 100644 src/client/fontengine.cpp create mode 100644 src/client/fontengine.h create mode 100644 src/client/game.cpp create mode 100644 src/client/game.h create mode 100644 src/client/gameui.cpp create mode 100644 src/client/gameui.h create mode 100644 src/client/guiscalingfilter.cpp create mode 100644 src/client/guiscalingfilter.h create mode 100644 src/client/hud.cpp create mode 100644 src/client/hud.h create mode 100644 src/client/imagefilters.cpp create mode 100644 src/client/imagefilters.h create mode 100644 src/client/inputhandler.cpp create mode 100644 src/client/inputhandler.h create mode 100644 src/client/joystick_controller.cpp create mode 100644 src/client/joystick_controller.h create mode 100644 src/client/keycode.cpp create mode 100644 src/client/keycode.h create mode 100644 src/client/keys.h create mode 100644 src/client/localplayer.cpp create mode 100644 src/client/localplayer.h create mode 100644 src/client/mapblock_mesh.cpp create mode 100644 src/client/mapblock_mesh.h create mode 100644 src/client/mesh.cpp create mode 100644 src/client/mesh.h create mode 100644 src/client/mesh_generator_thread.cpp create mode 100644 src/client/mesh_generator_thread.h create mode 100644 src/client/meshgen/collector.cpp create mode 100644 src/client/meshgen/collector.h create mode 100644 src/client/minimap.cpp create mode 100644 src/client/minimap.h create mode 100644 src/client/particles.cpp create mode 100644 src/client/particles.h create mode 100644 src/client/render/anaglyph.cpp create mode 100644 src/client/render/anaglyph.h create mode 100644 src/client/render/core.cpp create mode 100644 src/client/render/core.h create mode 100644 src/client/render/factory.cpp create mode 100644 src/client/render/factory.h create mode 100644 src/client/render/interlaced.cpp create mode 100644 src/client/render/interlaced.h create mode 100644 src/client/render/pageflip.cpp create mode 100644 src/client/render/pageflip.h create mode 100644 src/client/render/plain.cpp create mode 100644 src/client/render/plain.h create mode 100644 src/client/render/sidebyside.cpp create mode 100644 src/client/render/sidebyside.h create mode 100644 src/client/render/stereo.cpp create mode 100644 src/client/render/stereo.h create mode 100644 src/client/renderingengine.cpp create mode 100644 src/client/renderingengine.h create mode 100644 src/client/shader.cpp create mode 100644 src/client/shader.h create mode 100644 src/client/shadows/dynamicshadows.cpp create mode 100644 src/client/shadows/dynamicshadows.h create mode 100644 src/client/shadows/dynamicshadowsrender.cpp create mode 100644 src/client/shadows/dynamicshadowsrender.h create mode 100644 src/client/shadows/shadowsScreenQuad.cpp create mode 100644 src/client/shadows/shadowsScreenQuad.h create mode 100644 src/client/shadows/shadowsshadercallbacks.cpp create mode 100644 src/client/shadows/shadowsshadercallbacks.h create mode 100644 src/client/sky.cpp create mode 100644 src/client/sky.h create mode 100644 src/client/sound.cpp create mode 100644 src/client/sound.h create mode 100644 src/client/sound_openal.cpp create mode 100644 src/client/sound_openal.h create mode 100644 src/client/tile.cpp create mode 100644 src/client/tile.h create mode 100644 src/client/wieldmesh.cpp create mode 100644 src/client/wieldmesh.h create mode 100644 src/clientiface.cpp create mode 100644 src/clientiface.h create mode 100644 src/cmake_config.h.in create mode 100644 src/cmake_config_githash.h.in create mode 100644 src/collision.cpp create mode 100644 src/collision.h create mode 100644 src/config.h create mode 100644 src/constants.h create mode 100644 src/content/CMakeLists.txt create mode 100644 src/content/content.cpp create mode 100644 src/content/content.h create mode 100644 src/content/mod_configuration.cpp create mode 100644 src/content/mod_configuration.h create mode 100644 src/content/mods.cpp create mode 100644 src/content/mods.h create mode 100644 src/content/subgames.cpp create mode 100644 src/content/subgames.h create mode 100644 src/content_mapnode.cpp create mode 100644 src/content_mapnode.h create mode 100644 src/content_nodemeta.cpp create mode 100644 src/content_nodemeta.h create mode 100644 src/convert_json.cpp create mode 100644 src/convert_json.h create mode 100644 src/craftdef.cpp create mode 100644 src/craftdef.h create mode 100644 src/database/CMakeLists.txt create mode 100644 src/database/database-dummy.cpp create mode 100644 src/database/database-dummy.h create mode 100644 src/database/database-files.cpp create mode 100644 src/database/database-files.h create mode 100644 src/database/database-leveldb.cpp create mode 100644 src/database/database-leveldb.h create mode 100644 src/database/database-postgresql.cpp create mode 100644 src/database/database-postgresql.h create mode 100644 src/database/database-redis.cpp create mode 100644 src/database/database-redis.h create mode 100644 src/database/database-sqlite3.cpp create mode 100644 src/database/database-sqlite3.h create mode 100644 src/database/database.cpp create mode 100644 src/database/database.h create mode 100644 src/daynightratio.h create mode 100644 src/debug.cpp create mode 100644 src/debug.h create mode 100644 src/defaultsettings.cpp create mode 100644 src/defaultsettings.h create mode 100644 src/emerge.cpp create mode 100644 src/emerge.h create mode 100644 src/environment.cpp create mode 100644 src/environment.h create mode 100644 src/exceptions.h create mode 100644 src/face_position_cache.cpp create mode 100644 src/face_position_cache.h create mode 100644 src/filesys.cpp create mode 100644 src/filesys.h create mode 100644 src/gamedef.h create mode 100644 src/gameparams.h create mode 100644 src/gettext.cpp create mode 100644 src/gettext.h create mode 100644 src/gettime.h create mode 100644 src/gui/CMakeLists.txt create mode 100644 src/gui/StyleSpec.h create mode 100644 src/gui/guiAnimatedImage.cpp create mode 100644 src/gui/guiAnimatedImage.h create mode 100644 src/gui/guiBackgroundImage.cpp create mode 100644 src/gui/guiBackgroundImage.h create mode 100644 src/gui/guiBox.cpp create mode 100644 src/gui/guiBox.h create mode 100644 src/gui/guiButton.cpp create mode 100644 src/gui/guiButton.h create mode 100644 src/gui/guiButtonImage.cpp create mode 100644 src/gui/guiButtonImage.h create mode 100644 src/gui/guiButtonItemImage.cpp create mode 100644 src/gui/guiButtonItemImage.h create mode 100644 src/gui/guiChatConsole.cpp create mode 100644 src/gui/guiChatConsole.h create mode 100644 src/gui/guiEditBox.cpp create mode 100644 src/gui/guiEditBox.h create mode 100644 src/gui/guiEditBoxWithScrollbar.cpp create mode 100644 src/gui/guiEditBoxWithScrollbar.h create mode 100644 src/gui/guiEngine.cpp create mode 100644 src/gui/guiEngine.h create mode 100644 src/gui/guiFormSpecMenu.cpp create mode 100644 src/gui/guiFormSpecMenu.h create mode 100644 src/gui/guiHyperText.cpp create mode 100644 src/gui/guiHyperText.h create mode 100644 src/gui/guiInventoryList.cpp create mode 100644 src/gui/guiInventoryList.h create mode 100644 src/gui/guiItemImage.cpp create mode 100644 src/gui/guiItemImage.h create mode 100644 src/gui/guiKeyChangeMenu.cpp create mode 100644 src/gui/guiKeyChangeMenu.h create mode 100644 src/gui/guiMainMenu.h create mode 100644 src/gui/guiPasswordChange.cpp create mode 100644 src/gui/guiPasswordChange.h create mode 100644 src/gui/guiPathSelectMenu.cpp create mode 100644 src/gui/guiPathSelectMenu.h create mode 100644 src/gui/guiScene.cpp create mode 100644 src/gui/guiScene.h create mode 100644 src/gui/guiScrollBar.cpp create mode 100644 src/gui/guiScrollBar.h create mode 100644 src/gui/guiScrollContainer.cpp create mode 100644 src/gui/guiScrollContainer.h create mode 100644 src/gui/guiSkin.cpp create mode 100644 src/gui/guiSkin.h create mode 100644 src/gui/guiTable.cpp create mode 100644 src/gui/guiTable.h create mode 100644 src/gui/guiVolumeChange.cpp create mode 100644 src/gui/guiVolumeChange.h create mode 100644 src/gui/mainmenumanager.h create mode 100644 src/gui/modalMenu.cpp create mode 100644 src/gui/modalMenu.h create mode 100644 src/gui/profilergraph.cpp create mode 100644 src/gui/profilergraph.h create mode 100644 src/gui/touchscreengui.cpp create mode 100644 src/gui/touchscreengui.h create mode 100644 src/httpfetch.cpp create mode 100644 src/httpfetch.h create mode 100644 src/hud.cpp create mode 100644 src/hud.h create mode 100644 src/inventory.cpp create mode 100644 src/inventory.h create mode 100644 src/inventorymanager.cpp create mode 100644 src/inventorymanager.h create mode 100644 src/irr_aabb3d.h create mode 100644 src/irr_ptr.h create mode 100644 src/irr_v2d.h create mode 100644 src/irr_v3d.h create mode 100644 src/irrlicht_changes/CGUITTFont.cpp create mode 100644 src/irrlicht_changes/CGUITTFont.h create mode 100644 src/irrlicht_changes/CMakeLists.txt create mode 100644 src/irrlicht_changes/static_text.cpp create mode 100644 src/irrlicht_changes/static_text.h create mode 100644 src/irrlichttypes.h create mode 100644 src/irrlichttypes_bloated.h create mode 100644 src/irrlichttypes_extrabloated.h create mode 100644 src/itemdef.cpp create mode 100644 src/itemdef.h create mode 100644 src/itemgroup.h create mode 100644 src/itemstackmetadata.cpp create mode 100644 src/itemstackmetadata.h create mode 100644 src/light.cpp create mode 100644 src/light.h create mode 100644 src/lighting.h create mode 100644 src/log.cpp create mode 100644 src/log.h create mode 100644 src/main.cpp create mode 100644 src/map.cpp create mode 100644 src/map.h create mode 100644 src/map_settings_manager.cpp create mode 100644 src/map_settings_manager.h create mode 100644 src/mapblock.cpp create mode 100644 src/mapblock.h create mode 100644 src/mapgen/CMakeLists.txt create mode 100644 src/mapgen/cavegen.cpp create mode 100644 src/mapgen/cavegen.h create mode 100644 src/mapgen/dungeongen.cpp create mode 100644 src/mapgen/dungeongen.h create mode 100644 src/mapgen/mapgen.cpp create mode 100644 src/mapgen/mapgen.h create mode 100644 src/mapgen/mapgen_carpathian.cpp create mode 100644 src/mapgen/mapgen_carpathian.h create mode 100644 src/mapgen/mapgen_flat.cpp create mode 100644 src/mapgen/mapgen_flat.h create mode 100644 src/mapgen/mapgen_fractal.cpp create mode 100644 src/mapgen/mapgen_fractal.h create mode 100644 src/mapgen/mapgen_singlenode.cpp create mode 100644 src/mapgen/mapgen_singlenode.h create mode 100644 src/mapgen/mapgen_v5.cpp create mode 100644 src/mapgen/mapgen_v5.h create mode 100644 src/mapgen/mapgen_v6.cpp create mode 100644 src/mapgen/mapgen_v6.h create mode 100644 src/mapgen/mapgen_v7.cpp create mode 100644 src/mapgen/mapgen_v7.h create mode 100644 src/mapgen/mapgen_valleys.cpp create mode 100644 src/mapgen/mapgen_valleys.h create mode 100644 src/mapgen/mg_biome.cpp create mode 100644 src/mapgen/mg_biome.h create mode 100644 src/mapgen/mg_decoration.cpp create mode 100644 src/mapgen/mg_decoration.h create mode 100644 src/mapgen/mg_ore.cpp create mode 100644 src/mapgen/mg_ore.h create mode 100644 src/mapgen/mg_schematic.cpp create mode 100644 src/mapgen/mg_schematic.h create mode 100644 src/mapgen/treegen.cpp create mode 100644 src/mapgen/treegen.h create mode 100644 src/mapnode.cpp create mode 100644 src/mapnode.h create mode 100644 src/mapsector.cpp create mode 100644 src/mapsector.h create mode 100644 src/metadata.cpp create mode 100644 src/metadata.h create mode 100644 src/modchannels.cpp create mode 100644 src/modchannels.h create mode 100644 src/modifiedstate.h create mode 100644 src/mtevent.h create mode 100644 src/nameidmapping.cpp create mode 100644 src/nameidmapping.h create mode 100644 src/network/CMakeLists.txt create mode 100644 src/network/address.cpp create mode 100644 src/network/address.h create mode 100644 src/network/clientopcodes.cpp create mode 100644 src/network/clientopcodes.h create mode 100644 src/network/clientpackethandler.cpp create mode 100644 src/network/connection.cpp create mode 100644 src/network/connection.h create mode 100644 src/network/connectionthreads.cpp create mode 100644 src/network/connectionthreads.h create mode 100644 src/network/networkexceptions.h create mode 100644 src/network/networkpacket.cpp create mode 100644 src/network/networkpacket.h create mode 100644 src/network/networkprotocol.h create mode 100644 src/network/peerhandler.h create mode 100644 src/network/serveropcodes.cpp create mode 100644 src/network/serveropcodes.h create mode 100644 src/network/serverpackethandler.cpp create mode 100644 src/network/socket.cpp create mode 100644 src/network/socket.h create mode 100644 src/nodedef.cpp create mode 100644 src/nodedef.h create mode 100644 src/nodemetadata.cpp create mode 100644 src/nodemetadata.h create mode 100644 src/nodetimer.cpp create mode 100644 src/nodetimer.h create mode 100644 src/noise.cpp create mode 100644 src/noise.h create mode 100644 src/objdef.cpp create mode 100644 src/objdef.h create mode 100644 src/object_properties.cpp create mode 100644 src/object_properties.h create mode 100644 src/particles.cpp create mode 100644 src/particles.h create mode 100644 src/pathfinder.cpp create mode 100644 src/pathfinder.h create mode 100644 src/player.cpp create mode 100644 src/player.h create mode 100644 src/porting.cpp create mode 100644 src/porting.h create mode 100644 src/porting_android.cpp create mode 100644 src/porting_android.h create mode 100644 src/profiler.cpp create mode 100644 src/profiler.h create mode 100644 src/raycast.cpp create mode 100644 src/raycast.h create mode 100644 src/reflowscan.cpp create mode 100644 src/reflowscan.h create mode 100644 src/remoteplayer.cpp create mode 100644 src/remoteplayer.h create mode 100644 src/rollback.cpp create mode 100644 src/rollback.h create mode 100644 src/rollback_interface.cpp create mode 100644 src/rollback_interface.h create mode 100644 src/script/CMakeLists.txt create mode 100644 src/script/common/CMakeLists.txt create mode 100644 src/script/common/c_content.cpp create mode 100644 src/script/common/c_content.h create mode 100644 src/script/common/c_converter.cpp create mode 100644 src/script/common/c_converter.h create mode 100644 src/script/common/c_internal.cpp create mode 100644 src/script/common/c_internal.h create mode 100644 src/script/common/c_packer.cpp create mode 100644 src/script/common/c_packer.h create mode 100644 src/script/common/c_types.cpp create mode 100644 src/script/common/c_types.h create mode 100644 src/script/common/helper.cpp create mode 100644 src/script/common/helper.h create mode 100644 src/script/cpp_api/CMakeLists.txt create mode 100644 src/script/cpp_api/s_async.cpp create mode 100644 src/script/cpp_api/s_async.h create mode 100644 src/script/cpp_api/s_base.cpp create mode 100644 src/script/cpp_api/s_base.h create mode 100644 src/script/cpp_api/s_client.cpp create mode 100644 src/script/cpp_api/s_client.h create mode 100644 src/script/cpp_api/s_entity.cpp create mode 100644 src/script/cpp_api/s_entity.h create mode 100644 src/script/cpp_api/s_env.cpp create mode 100644 src/script/cpp_api/s_env.h create mode 100644 src/script/cpp_api/s_internal.h create mode 100644 src/script/cpp_api/s_inventory.cpp create mode 100644 src/script/cpp_api/s_inventory.h create mode 100644 src/script/cpp_api/s_item.cpp create mode 100644 src/script/cpp_api/s_item.h create mode 100644 src/script/cpp_api/s_mainmenu.cpp create mode 100644 src/script/cpp_api/s_mainmenu.h create mode 100644 src/script/cpp_api/s_modchannels.cpp create mode 100644 src/script/cpp_api/s_modchannels.h create mode 100644 src/script/cpp_api/s_node.cpp create mode 100644 src/script/cpp_api/s_node.h create mode 100644 src/script/cpp_api/s_nodemeta.cpp create mode 100644 src/script/cpp_api/s_nodemeta.h create mode 100644 src/script/cpp_api/s_player.cpp create mode 100644 src/script/cpp_api/s_player.h create mode 100644 src/script/cpp_api/s_security.cpp create mode 100644 src/script/cpp_api/s_security.h create mode 100644 src/script/cpp_api/s_server.cpp create mode 100644 src/script/cpp_api/s_server.h create mode 100644 src/script/lua_api/CMakeLists.txt create mode 100644 src/script/lua_api/l_areastore.cpp create mode 100644 src/script/lua_api/l_areastore.h create mode 100644 src/script/lua_api/l_auth.cpp create mode 100644 src/script/lua_api/l_auth.h create mode 100644 src/script/lua_api/l_base.cpp create mode 100644 src/script/lua_api/l_base.h create mode 100644 src/script/lua_api/l_camera.cpp create mode 100644 src/script/lua_api/l_camera.h create mode 100644 src/script/lua_api/l_client.cpp create mode 100644 src/script/lua_api/l_client.h create mode 100644 src/script/lua_api/l_craft.cpp create mode 100644 src/script/lua_api/l_craft.h create mode 100644 src/script/lua_api/l_env.cpp create mode 100644 src/script/lua_api/l_env.h create mode 100644 src/script/lua_api/l_http.cpp create mode 100644 src/script/lua_api/l_http.h create mode 100644 src/script/lua_api/l_internal.h create mode 100644 src/script/lua_api/l_inventory.cpp create mode 100644 src/script/lua_api/l_inventory.h create mode 100644 src/script/lua_api/l_item.cpp create mode 100644 src/script/lua_api/l_item.h create mode 100644 src/script/lua_api/l_itemstackmeta.cpp create mode 100644 src/script/lua_api/l_itemstackmeta.h create mode 100644 src/script/lua_api/l_localplayer.cpp create mode 100644 src/script/lua_api/l_localplayer.h create mode 100644 src/script/lua_api/l_mainmenu.cpp create mode 100644 src/script/lua_api/l_mainmenu.h create mode 100644 src/script/lua_api/l_mapgen.cpp create mode 100644 src/script/lua_api/l_mapgen.h create mode 100644 src/script/lua_api/l_metadata.cpp create mode 100644 src/script/lua_api/l_metadata.h create mode 100644 src/script/lua_api/l_minimap.cpp create mode 100644 src/script/lua_api/l_minimap.h create mode 100644 src/script/lua_api/l_modchannels.cpp create mode 100644 src/script/lua_api/l_modchannels.h create mode 100644 src/script/lua_api/l_nodemeta.cpp create mode 100644 src/script/lua_api/l_nodemeta.h create mode 100644 src/script/lua_api/l_nodetimer.cpp create mode 100644 src/script/lua_api/l_nodetimer.h create mode 100644 src/script/lua_api/l_noise.cpp create mode 100644 src/script/lua_api/l_noise.h create mode 100644 src/script/lua_api/l_object.cpp create mode 100644 src/script/lua_api/l_object.h create mode 100644 src/script/lua_api/l_particleparams.h create mode 100644 src/script/lua_api/l_particles.cpp create mode 100644 src/script/lua_api/l_particles.h create mode 100644 src/script/lua_api/l_particles_local.cpp create mode 100644 src/script/lua_api/l_particles_local.h create mode 100644 src/script/lua_api/l_playermeta.cpp create mode 100644 src/script/lua_api/l_playermeta.h create mode 100644 src/script/lua_api/l_rollback.cpp create mode 100644 src/script/lua_api/l_rollback.h create mode 100644 src/script/lua_api/l_server.cpp create mode 100644 src/script/lua_api/l_server.h create mode 100644 src/script/lua_api/l_settings.cpp create mode 100644 src/script/lua_api/l_settings.h create mode 100644 src/script/lua_api/l_sound.cpp create mode 100644 src/script/lua_api/l_sound.h create mode 100644 src/script/lua_api/l_storage.cpp create mode 100644 src/script/lua_api/l_storage.h create mode 100644 src/script/lua_api/l_util.cpp create mode 100644 src/script/lua_api/l_util.h create mode 100644 src/script/lua_api/l_vmanip.cpp create mode 100644 src/script/lua_api/l_vmanip.h create mode 100644 src/script/scripting_client.cpp create mode 100644 src/script/scripting_client.h create mode 100644 src/script/scripting_mainmenu.cpp create mode 100644 src/script/scripting_mainmenu.h create mode 100644 src/script/scripting_server.cpp create mode 100644 src/script/scripting_server.h create mode 100644 src/serialization.cpp create mode 100644 src/serialization.h create mode 100644 src/server.cpp create mode 100644 src/server.h create mode 100644 src/server/CMakeLists.txt create mode 100644 src/server/activeobjectmgr.cpp create mode 100644 src/server/activeobjectmgr.h create mode 100644 src/server/luaentity_sao.cpp create mode 100644 src/server/luaentity_sao.h create mode 100644 src/server/mods.cpp create mode 100644 src/server/mods.h create mode 100644 src/server/player_sao.cpp create mode 100644 src/server/player_sao.h create mode 100644 src/server/serveractiveobject.cpp create mode 100644 src/server/serveractiveobject.h create mode 100644 src/server/serverinventorymgr.cpp create mode 100644 src/server/serverinventorymgr.h create mode 100644 src/server/unit_sao.cpp create mode 100644 src/server/unit_sao.h create mode 100644 src/serverenvironment.cpp create mode 100644 src/serverenvironment.h create mode 100644 src/serverlist.cpp create mode 100644 src/serverlist.h create mode 100644 src/settings.cpp create mode 100644 src/settings.h create mode 100644 src/settings_translation_file.cpp create mode 100644 src/skyparams.h create mode 100644 src/sound.h create mode 100644 src/staticobject.cpp create mode 100644 src/staticobject.h create mode 100644 src/terminal_chat_console.cpp create mode 100644 src/terminal_chat_console.h create mode 100644 src/texture_override.cpp create mode 100644 src/texture_override.h create mode 100644 src/threading/CMakeLists.txt create mode 100644 src/threading/event.cpp create mode 100644 src/threading/event.h create mode 100644 src/threading/mutex_auto_lock.h create mode 100644 src/threading/semaphore.cpp create mode 100644 src/threading/semaphore.h create mode 100644 src/threading/thread.cpp create mode 100644 src/threading/thread.h create mode 100644 src/tileanimation.cpp create mode 100644 src/tileanimation.h create mode 100644 src/tool.cpp create mode 100644 src/tool.h create mode 100644 src/translation.cpp create mode 100644 src/translation.h create mode 100644 src/unittest/CMakeLists.txt create mode 100644 src/unittest/test.cpp create mode 100644 src/unittest/test.h create mode 100644 src/unittest/test_activeobject.cpp create mode 100644 src/unittest/test_address.cpp create mode 100644 src/unittest/test_areastore.cpp create mode 100644 src/unittest/test_authdatabase.cpp create mode 100644 src/unittest/test_ban.cpp create mode 100644 src/unittest/test_clientactiveobjectmgr.cpp create mode 100644 src/unittest/test_collision.cpp create mode 100644 src/unittest/test_compression.cpp create mode 100644 src/unittest/test_config.h.in create mode 100644 src/unittest/test_connection.cpp create mode 100644 src/unittest/test_eventmanager.cpp create mode 100644 src/unittest/test_filepath.cpp create mode 100644 src/unittest/test_gameui.cpp create mode 100644 src/unittest/test_gettext.cpp create mode 100644 src/unittest/test_inventory.cpp create mode 100644 src/unittest/test_irrptr.cpp create mode 100644 src/unittest/test_keycode.cpp create mode 100644 src/unittest/test_lua.cpp create mode 100644 src/unittest/test_map.cpp create mode 100644 src/unittest/test_map_settings_manager.cpp create mode 100644 src/unittest/test_mapnode.cpp create mode 100644 src/unittest/test_mod/test_mod/init.lua create mode 100644 src/unittest/test_mod/test_mod/mod.conf create mode 100644 src/unittest/test_modchannels.cpp create mode 100644 src/unittest/test_modmetadatadatabase.cpp create mode 100644 src/unittest/test_nodedef.cpp create mode 100644 src/unittest/test_noderesolver.cpp create mode 100644 src/unittest/test_noise.cpp create mode 100644 src/unittest/test_objdef.cpp create mode 100644 src/unittest/test_profiler.cpp create mode 100644 src/unittest/test_random.cpp create mode 100644 src/unittest/test_schematic.cpp create mode 100644 src/unittest/test_serialization.cpp create mode 100644 src/unittest/test_server_shutdown_state.cpp create mode 100644 src/unittest/test_serveractiveobjectmgr.cpp create mode 100644 src/unittest/test_servermodmanager.cpp create mode 100644 src/unittest/test_settings.cpp create mode 100644 src/unittest/test_socket.cpp create mode 100644 src/unittest/test_threading.cpp create mode 100644 src/unittest/test_utilities.cpp create mode 100644 src/unittest/test_voxelalgorithms.cpp create mode 100644 src/unittest/test_voxelarea.cpp create mode 100644 src/unittest/test_voxelmanipulator.cpp create mode 100644 src/unittest/test_world/do_not_remove.txt create mode 100644 src/util/CMakeLists.txt create mode 100644 src/util/Optional.h create mode 100644 src/util/areastore.cpp create mode 100644 src/util/areastore.h create mode 100644 src/util/auth.cpp create mode 100644 src/util/auth.h create mode 100644 src/util/base64.cpp create mode 100644 src/util/base64.h create mode 100644 src/util/basic_macros.h create mode 100644 src/util/container.h create mode 100644 src/util/directiontables.cpp create mode 100644 src/util/directiontables.h create mode 100644 src/util/enriched_string.cpp create mode 100644 src/util/enriched_string.h create mode 100644 src/util/hex.h create mode 100644 src/util/ieee_float.cpp create mode 100644 src/util/ieee_float.h create mode 100644 src/util/md32_common.h create mode 100644 src/util/metricsbackend.cpp create mode 100644 src/util/metricsbackend.h create mode 100644 src/util/numeric.cpp create mode 100644 src/util/numeric.h create mode 100644 src/util/png.cpp create mode 100644 src/util/png.h create mode 100644 src/util/pointedthing.cpp create mode 100644 src/util/pointedthing.h create mode 100644 src/util/pointer.h create mode 100644 src/util/quicktune.cpp create mode 100644 src/util/quicktune.h create mode 100644 src/util/quicktune_shortcutter.h create mode 100644 src/util/serialize.cpp create mode 100644 src/util/serialize.h create mode 100644 src/util/sha1.cpp create mode 100644 src/util/sha1.h create mode 100644 src/util/sha2.h create mode 100644 src/util/sha256.c create mode 100644 src/util/srp.cpp create mode 100644 src/util/srp.h create mode 100644 src/util/stream.h create mode 100644 src/util/strfnd.h create mode 100644 src/util/string.cpp create mode 100644 src/util/string.h create mode 100644 src/util/thread.h create mode 100644 src/util/timetaker.cpp create mode 100644 src/util/timetaker.h create mode 100644 src/version.cpp create mode 100644 src/version.h create mode 100644 src/voxel.cpp create mode 100644 src/voxel.h create mode 100644 src/voxelalgorithms.cpp create mode 100644 src/voxelalgorithms.h create mode 100644 textures/base/pack/air.png create mode 100644 textures/base/pack/aux1_btn.png create mode 100644 textures/base/pack/blank.png create mode 100644 textures/base/pack/bubble.png create mode 100644 textures/base/pack/bubble_gone.png create mode 100644 textures/base/pack/camera_btn.png create mode 100644 textures/base/pack/cdb_add.png create mode 100644 textures/base/pack/cdb_clear.png create mode 100644 textures/base/pack/cdb_downloading.png create mode 100644 textures/base/pack/cdb_queued.png create mode 100644 textures/base/pack/cdb_update.png create mode 100644 textures/base/pack/cdb_viewonline.png create mode 100644 textures/base/pack/chat_btn.png create mode 100644 textures/base/pack/chat_hide_btn.png create mode 100644 textures/base/pack/chat_show_btn.png create mode 100644 textures/base/pack/checkbox_16.png create mode 100644 textures/base/pack/checkbox_16_white.png create mode 100644 textures/base/pack/checkbox_32.png create mode 100644 textures/base/pack/checkbox_64.png create mode 100644 textures/base/pack/clear.png create mode 100644 textures/base/pack/crack_anylength.png create mode 100644 textures/base/pack/debug_btn.png create mode 100644 textures/base/pack/down.png create mode 100644 textures/base/pack/drop_btn.png create mode 100644 textures/base/pack/end_icon.png create mode 100644 textures/base/pack/error_icon_orange.png create mode 100644 textures/base/pack/error_icon_red.png create mode 100644 textures/base/pack/error_screenshot.png create mode 100644 textures/base/pack/fast_btn.png create mode 100644 textures/base/pack/fly_btn.png create mode 100644 textures/base/pack/gear_icon.png create mode 100644 textures/base/pack/halo.png create mode 100644 textures/base/pack/heart.png create mode 100644 textures/base/pack/heart_gone.png create mode 100644 textures/base/pack/ignore.png create mode 100644 textures/base/pack/inventory_btn.png create mode 100644 textures/base/pack/joystick_bg.png create mode 100644 textures/base/pack/joystick_center.png create mode 100644 textures/base/pack/joystick_off.png create mode 100644 textures/base/pack/jump_btn.png create mode 100644 textures/base/pack/loading_screenshot.png create mode 100644 textures/base/pack/logo.png create mode 100644 textures/base/pack/menu_bg.png create mode 100644 textures/base/pack/menu_header.png create mode 100644 textures/base/pack/minimap_btn.png create mode 100644 textures/base/pack/minimap_mask_round.png create mode 100644 textures/base/pack/minimap_mask_square.png create mode 100644 textures/base/pack/minimap_overlay_round.png create mode 100644 textures/base/pack/minimap_overlay_square.png create mode 100644 textures/base/pack/next_icon.png create mode 100644 textures/base/pack/no_screenshot.png create mode 100644 textures/base/pack/no_texture.png create mode 100644 textures/base/pack/no_texture_airlike.png create mode 100644 textures/base/pack/noclip_btn.png create mode 100644 textures/base/pack/object_marker_red.png create mode 100644 textures/base/pack/player.png create mode 100644 textures/base/pack/player_back.png create mode 100644 textures/base/pack/player_marker.png create mode 100644 textures/base/pack/plus.png create mode 100644 textures/base/pack/prev_icon.png create mode 100644 textures/base/pack/progress_bar.png create mode 100644 textures/base/pack/progress_bar_bg.png create mode 100644 textures/base/pack/rangeview_btn.png create mode 100644 textures/base/pack/rare_controls.png create mode 100644 textures/base/pack/refresh.png create mode 100644 textures/base/pack/search.png create mode 100644 textures/base/pack/server_favorite.png create mode 100644 textures/base/pack/server_favorite_delete.png create mode 100644 textures/base/pack/server_flags_creative.png create mode 100644 textures/base/pack/server_flags_damage.png create mode 100644 textures/base/pack/server_flags_pvp.png create mode 100644 textures/base/pack/server_incompatible.png create mode 100644 textures/base/pack/server_ping_1.png create mode 100644 textures/base/pack/server_ping_2.png create mode 100644 textures/base/pack/server_ping_3.png create mode 100644 textures/base/pack/server_ping_4.png create mode 100644 textures/base/pack/server_public.png create mode 100644 textures/base/pack/smoke_puff.png create mode 100644 textures/base/pack/start_icon.png create mode 100644 textures/base/pack/sunrisebg.png create mode 100644 textures/base/pack/unknown_item.png create mode 100644 textures/base/pack/unknown_node.png create mode 100644 textures/base/pack/unknown_object.png create mode 100644 textures/base/pack/wieldhand.png create mode 100644 textures/base/pack/zoom.png create mode 100644 util/buildbot/buildwin32.sh create mode 100644 util/buildbot/buildwin64.sh create mode 100644 util/buildbot/toolchain_i686-w64-mingw32-posix.cmake create mode 100644 util/buildbot/toolchain_i686-w64-mingw32.cmake create mode 100644 util/buildbot/toolchain_x86_64-w64-mingw32-posix.cmake create mode 100644 util/buildbot/toolchain_x86_64-w64-mingw32.cmake create mode 100644 util/bump_version.sh create mode 100644 util/ci/build.sh create mode 100644 util/ci/build_prometheus_cpp.sh create mode 100644 util/ci/clang-format-whitelist.txt create mode 100644 util/ci/clang-format.sh create mode 100644 util/ci/clang-tidy.sh create mode 100644 util/ci/common.sh create mode 100644 util/ci/run-clang-tidy.py create mode 100644 util/fix_format.sh create mode 100644 util/gather_git_credits.py create mode 100644 util/helper_mod/init.lua create mode 100644 util/helper_mod/mod.conf create mode 100644 util/reorder_translation_commits.py create mode 100644 util/stress_mapgen.sh create mode 100644 util/test_multiplayer.sh create mode 100644 util/updatepo.sh create mode 100644 util/wireshark/minetest.lua diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..63f12b6 --- /dev/null +++ b/.clang-format @@ -0,0 +1,33 @@ +BasedOnStyle: LLVM +IndentWidth: 4 +UseTab: Always +TabWidth: 4 +BreakBeforeBraces: Custom +Standard: Cpp11 +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + BeforeCatch: false + BeforeElse: false +FixNamespaceComments: false +AllowShortIfStatementsOnASingleLine: false +IndentCaseLabels: false +AccessModifierOffset: -4 +ColumnLimit: 90 +AllowShortFunctionsOnASingleLine: InlineOnly +SortIncludes: false +IncludeCategories: + - Regex: '^".*' + Priority: 2 + - Regex: '^<.*' + Priority: 1 +AlignAfterOpenBracket: DontAlign +ContinuationIndentWidth: 8 +ConstructorInitializerIndentWidth: 8 +BreakConstructorInitializers: AfterColon +AlwaysBreakTemplateDeclarations: Yes diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..1b9f8bd --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,5 @@ +Checks: '-*,modernize-use-emplace,modernize-avoid-bind,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,performance-*' +WarningsAsErrors: '-*,modernize-use-emplace,performance-type-promotion-in-math-fn,performance-faster-string-find,performance-implicit-cast-in-loop' +CheckOptions: + - key: performance-unnecessary-value-param.AllowedTypes + value: v[23]f;v[23][su](16|32) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bda43eb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +./cmake-build-* +./build/* +./cache/* +Dockerfile diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ec06452 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +[*] +end_of_line = lf + +[*.{cpp,h,lua,txt,glsl,md,c,cmake,java,gradle}] +charset = utf8 +indent_size = 4 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2e62a4e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.cpp diff=cpp +*.h diff=cpp diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..f60f584 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,183 @@ +# Contributing + +Contributions are welcome! Here's how you can help: + +- [Contributing code](#code) +- [Reporting issues](#issues) +- [Requesting features](#feature-requests) +- [Translating](#translations) +- [Donating](#donations) + +## Code + +1. [Fork](https://help.github.com/articles/fork-a-repo/) the repository and + [clone](https://help.github.com/articles/cloning-a-repository/) your fork. + +2. Before you start coding, consider opening an + [issue at Github](https://github.com/minetest/minetest/issues) to discuss the + suitability and implementation of your intended contribution with the core + developers. + + Any Pull Request that isn't a bug fix and isn't covered by + [the roadmap](../doc/direction.md) will be closed within a week unless it + receives a concept approval from a Core Developer. For this reason, it is + recommended that you open an issue for any such pull requests before doing + the work, to avoid disappointment. + + You may also benefit from discussing on our IRC development channel + [#minetest-dev](http://www.minetest.net/irc/). Note that a proper IRC client + is required to speak on this channel. + +3. Start coding! + - Refer to the + [Lua API](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt), + [Developer Wiki](http://dev.minetest.net/Main_Page) and other + [documentation](https://github.com/minetest/minetest/tree/master/doc). + - Follow the [C/C++](http://dev.minetest.net/Code_style_guidelines) and + [Lua](http://dev.minetest.net/Lua_code_style_guidelines) code style guidelines. + - Check your code works as expected and document any changes to the Lua API. + +4. Commit & [push](https://help.github.com/articles/pushing-to-a-remote/) your changes to a new branch (not `master`, one change per branch) + - Commit messages should: + - Use the present tense. + - Be descriptive. See the commit messages by core developers for examples. + - The first line should: + - Start with a capital letter. + - Be a compact summary of the commit. + - Preferably have less than 70 characters. + - Have no full stop at the end. + - The second line should be empty. + - The following lines should describe the commit, starting a new line for each point. + +5. Once you are happy with your changes, submit a pull request. + - Open the [pull-request form](https://github.com/minetest/minetest/pull/new/master). + - Add a description explaining what you've done (or if it's a + work-in-progress - what you need to do). + - Make sure to fill out the pull request template. + +### A pull-request is considered merge-able when: + +1. It follows [the roadmap](../doc/direction.md) in some way and fits the whole + picture of the project. +2. It works. +3. It follows the code style for + [C/C++](http://dev.minetest.net/Code_style_guidelines) or + [Lua](http://dev.minetest.net/Lua_code_style_guidelines). +4. The code's interfaces are well designed, regardless of other aspects that + might need more work in the future. +5. It uses protocols and formats which include the required compatibility. + +### Important note about automated GitHub checks + +When you submit a pull request, GitHub automatically runs checks on the Minetest +Engine combined with your changes. One of these checks is called 'cpp lint / +clang format', which checks code formatting. Because formatting for readability +requires human judgement this check often fails and often makes unsuitable +formatting requests which make code readability worse. + +If this check fails, look at the details to check for any clear mistakes and +correct those. However, you should not apply everything ClangFormat requests. +Ignore requests that make code readability worse and any other clearly +unsuitable requests. Discuss in the pull request with a core developer about how +to progress. + +## Issues + +If you experience an issue, we would like to know the details - especially when +a stable release is on the way. + +1. Do a quick search on GitHub to check if the issue has already been reported. +2. Is it an issue with the Minetest *engine*? If not, report it + [elsewhere](http://www.minetest.net/development/#reporting-issues). +3. [Open an issue](https://github.com/minetest/minetest/issues/new) and describe + the issue you are having - you could include: + - Error logs (check the bottom of the `debug.txt` file). + - Screenshots. + - Ways you have tried to solve the issue, and whether they worked or not. + - Your Minetest version and the content (games, mods or texture packs) you have installed. + - Your platform (e.g. Windows 10 or Ubuntu 15.04 x64). + +After reporting you should aim to answer questions or clarifications as this +helps pinpoint the cause of the issue (if you don't do this your issue may be +closed after 1 month). + +## Feature requests + +Feature requests are welcome but take a moment to see if your idea follows +[the roadmap](../doc/direction.md) in some way and fits the whole picture of +the project. You should provide a clear explanation with as much detail as +possible. + +## Translations + +The core translations of Minetest are performed using Weblate. You can access +the project page with a list of current languages +[here](https://hosted.weblate.org/projects/minetest/minetest/). + +Builtin (the component which contains things like server messages, chat command +descriptions, privilege descriptions) is translated separately; it needs to be +translated by editing a `.tr` text file. See +[Translation](https://dev.minetest.net/Translation) for more information. + +## Donations + +If you'd like to monetarily support Minetest development, you can find donation +methods on [our website](http://www.minetest.net/development/#donate). + +# Maintaining + +* This is a concise version of the + [Rules & Guidelines](http://dev.minetest.net/Category:Rules_and_Guidelines) on the developer wiki.* + +These notes are for those who have push access Minetest (core developers / maintainers). + +- See the [project organisation](http://dev.minetest.net/Organisation) for the people involved. + +## Concept approvals and roadmaps + +If a Pull Request is not a bug fix: + +* If it matches a goal in [the roadmap](../doc/direction.md), then the PR should + be labelled as "Roadmap" and the goal stated by number in the description. +* If it doesn't match a goal, then it needs to receive a concept approval within + a week of being opened to remain open. This 1 week deadline does not apply to + PRs opened before the roadmap was adopted; instead, they may remain open or be + closed as needed. Use the "Concept Approved" label. Issues can be marked as + "Concept Approved" to give preapproval to future PRs. + +## Reviewing pull requests + +Pull requests should be reviewed and, if appropriate, checked if they achieve +their intended purpose. You can show that you are in the process of, or will +review the pull request by commenting *"Looks good"* or something similar. + +**If the pull-request is not [merge-able](#a-pull-request-is-considered-merge-able-when):** + +Submit a comment explaining to the author what they need to change to make the +pull-request merge-able. + +- If the author comments or makes changes to the pull-request, it can be + reviewed again. +- If no response is made from the author within 1 month (when improvements are + suggested or a question is asked), it can be closed. + +**If the pull-request is [merge-able](#a-pull-request-is-considered-merge-able-when):** + +Submit a :+1: (+1) or "Looks good" comment to show you believe the pull-request should be merged. "Looks good" comments often signify that the patch might require (more) testing. + +- Two core developers must agree to the merge before it is carried out and both should +1 the pull request. +- Who intends to merge the pull-request should follow the commit rules: + - The title should follow the commit guidelines (title starts with a capital letter, present tense, descriptive). + - Don't modify history older than 10 minutes. + - Use rebase, not merge to get linear history: + - `curl https://github.com/minetest/minetest/pull/1.patch | git am` + +## Reviewing issues and feature requests + +- If an issue does not get a response from its author within 1 month (when requiring more details), it can be closed. +- When an issue is a duplicate, refer to the first ones and close the later ones. +- Tag issues with the appropriate [labels](https://github.com/minetest/minetest/labels) for devices, platforms etc. + +## Releasing a new version + +*Refer to [dev.minetest.net/Releasing_Minetest](http://dev.minetest.net/Releasing_Minetest)* diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..7cf34bd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: Unconfirmed bug +assignees: '' +--- + +##### Minetest version + +``` + +``` + +##### OS / Hardware + +Operating system: +CPU: + + +GPU model: +OpenGL version: + +##### Summary + + +##### Steps to reproduce + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ebcfa98 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: Feature request +assignees: '' +--- + +## Problem + +A clear and concise description of what the problem is. +ie: Why is this needed? +Ex. I'm always frustrated when [...] + +## Solutions + +A clear and concise description of what you want to happen. + +## Alternatives + +A clear and concise description of any alternative solutions or features you've considered. + +## Additional context + +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4132826 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,20 @@ +Add compact, short information about your PR for easier understanding: + +- Goal of the PR +- How does the PR work? +- Does it resolve any reported issue? +- Does this relate to a goal in [the roadmap](../doc/direction.md)? +- If not a bug fix, why is this PR needed? What usecases does it solve? + +## To do + +This PR is a Work in Progress / Ready for Review. + + +- [ ] List +- [ ] Things +- [ ] To do + +## How to test + + diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..e2dd043 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Supported Versions + +We only support the latest stable version for security issues. +See the [releases page](https://github.com/minetest/minetest/releases). + +## Reporting a Vulnerability + +We ask that you report vulnerabilities privately, by contacting a core developer, +to give us time to fix them. You can do that by emailing one of the following addresses: + +* celeron55@gmail.com +* rubenwardy@minetest.net + +Depending on severity, we will either create a private issue for the vulnerability +and release a patch version of Minetest, or give you permission to file the issue publicly. + +For more information on the justification of this policy, see +[Responsible Disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure). diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..8cbe5e0 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,52 @@ +name: android + +# build on c/cpp changes or workflow changes +on: + push: + paths: + - 'lib/**.[ch]' + - 'lib/**.cpp' + - 'src/**.[ch]' + - 'src/**.cpp' + - 'android/**' + - '.github/workflows/android.yml' + pull_request: + paths: + - 'lib/**.[ch]' + - 'lib/**.cpp' + - 'src/**.[ch]' + - 'src/**.cpp' + - 'android/**' + - '.github/workflows/android.yml' + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends gettext openjdk-11-jdk-headless + - name: Build with Gradle + run: cd android; ./gradlew assemblerelease + - name: Save armeabi artifact + uses: actions/upload-artifact@v3 + with: + name: Minetest-armeabi-v7a.apk + path: android/app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk + - name: Save arm64 artifact + uses: actions/upload-artifact@v3 + with: + name: Minetest-arm64-v8a.apk + path: android/app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk + - name: Save x86 artifact + uses: actions/upload-artifact@v3 + with: + name: Minetest-x86.apk + path: android/app/build/outputs/apk/release/app-x86-release-unsigned.apk + - name: Save x86_64 artifact + uses: actions/upload-artifact@v3 + with: + name: Minetest-x86_64.apk + path: android/app/build/outputs/apk/release/app-x86_64-release-unsigned.apk diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..282dbe3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,271 @@ +name: build + +# build on c/cpp changes or workflow changes +on: + push: + paths: + - 'lib/**.[ch]' + - 'lib/**.cpp' + - 'src/**.[ch]' + - 'src/**.cpp' + - '**/CMakeLists.txt' + - 'cmake/Modules/**' + - 'util/buildbot/**' + - 'util/ci/**' + - '.github/workflows/**.yml' + - 'Dockerfile' + - '.dockerignore' + pull_request: + paths: + - 'lib/**.[ch]' + - 'lib/**.cpp' + - 'src/**.[ch]' + - 'src/**.cpp' + - '**/CMakeLists.txt' + - 'cmake/Modules/**' + - 'util/buildbot/**' + - 'util/ci/**' + - '.github/workflows/**.yml' + - 'Dockerfile' + - '.dockerignore' + +jobs: + # Older gcc version (should be close to our minimum supported version) + gcc_5: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: | + source ./util/ci/common.sh + install_linux_deps g++-5 + + - name: Build + run: | + ./util/ci/build.sh + env: + CC: gcc-5 + CXX: g++-5 + + - name: Test + run: | + ./bin/minetest --run-unittests + + # Current gcc version + gcc_12: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: | + source ./util/ci/common.sh + install_linux_deps g++-12 libluajit-5.1-dev + + - name: Build + run: | + ./util/ci/build.sh + env: + CC: gcc-12 + CXX: g++-12 + + - name: Test + run: | + ./bin/minetest --run-unittests + + # Older clang version (should be close to our minimum supported version) + clang_3_9: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: | + source ./util/ci/common.sh + install_linux_deps clang-3.9 valgrind + + - name: Build + run: | + ./util/ci/build.sh + env: + CC: clang-3.9 + CXX: clang++-3.9 + + - name: Unittest + run: | + ./bin/minetest --run-unittests + + - name: Valgrind + run: | + valgrind --leak-check=full --leak-check-heuristics=all --undef-value-errors=no --error-exitcode=9 ./bin/minetest --run-unittests + + # Current clang version + clang_14: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: | + source ./util/ci/common.sh + install_linux_deps clang-14 gdb + + - name: Build + run: | + ./util/ci/build.sh + env: + CC: clang-14 + CXX: clang++-14 + + - name: Test + run: | + ./bin/minetest --run-unittests + + - name: Integration test + devtest + run: | + ./util/test_multiplayer.sh + + # Build with prometheus-cpp (server-only) + clang_9_prometheus: + name: "clang_9 (PROMETHEUS=1)" + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: | + source ./util/ci/common.sh + install_linux_deps clang-9 + + - name: Build prometheus-cpp + run: | + ./util/ci/build_prometheus_cpp.sh + + - name: Build + run: | + ./util/ci/build.sh + env: + CC: clang-9 + CXX: clang++-9 + CMAKE_FLAGS: "-DENABLE_PROMETHEUS=1 -DBUILD_CLIENT=0" + + - name: Test + run: | + ./bin/minetestserver --run-unittests + + docker: + name: "Docker image" + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Build docker image + run: | + docker build . -t minetest:latest + docker run --rm minetest:latest /usr/local/bin/minetestserver --version + + win32: + name: "MinGW cross-compiler (32-bit)" + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Install compiler + run: | + sudo apt-get update && sudo apt-get install -y gettext + wget http://minetest.kitsunemimi.pw/mingw-w64-i686_11.2.0_ubuntu20.04.tar.xz -O mingw.tar.xz + sudo tar -xaf mingw.tar.xz -C /usr + + - name: Build + run: | + EXISTING_MINETEST_DIR=$PWD ./util/buildbot/buildwin32.sh winbuild + env: + NO_MINETEST_GAME: 1 + NO_PACKAGE: 1 + + win64: + name: "MinGW cross-compiler (64-bit)" + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Install compiler + run: | + sudo apt-get update && sudo apt-get install -y gettext + wget http://minetest.kitsunemimi.pw/mingw-w64-x86_64_11.2.0_ubuntu20.04.tar.xz -O mingw.tar.xz + sudo tar -xaf mingw.tar.xz -C /usr + + - name: Build + run: | + EXISTING_MINETEST_DIR=$PWD ./util/buildbot/buildwin64.sh winbuild + env: + NO_MINETEST_GAME: 1 + NO_PACKAGE: 1 + + msvc: + name: VS 2019 ${{ matrix.config.arch }}-${{ matrix.type }} + runs-on: windows-2019 + env: + VCPKG_VERSION: 5cf60186a241e84e8232641ee973395d4fde90e1 + # 2022.02 + vcpkg_packages: zlib zstd curl[winssl] openal-soft libvorbis libogg libjpeg-turbo sqlite3 freetype luajit gmp jsoncpp opengl-registry + strategy: + fail-fast: false + matrix: + config: + - { + arch: x86, + generator: "-G'Visual Studio 16 2019' -A Win32", + vcpkg_triplet: x86-windows + } + - { + arch: x64, + generator: "-G'Visual Studio 16 2019' -A x64", + vcpkg_triplet: x64-windows + } + type: [portable] +# type: [portable, installer] +# The installer type is working, but disabled, to save runner jobs. +# Enable it, when working on the installer. + + steps: + - uses: actions/checkout@v3 + + - name: Checkout IrrlichtMt + run: | + $ref = @(Get-Content misc\irrlichtmt_tag.txt) + git clone https://github.com/minetest/irrlicht lib\irrlichtmt --depth 1 -b $ref[0] + + - name: Restore from cache and run vcpkg + uses: lukka/run-vcpkg@v7 + with: + vcpkgArguments: ${{env.vcpkg_packages}} + vcpkgDirectory: '${{ github.workspace }}\vcpkg' + appendedCacheKey: ${{ matrix.config.vcpkg_triplet }} + vcpkgGitCommitId: ${{ env.VCPKG_VERSION }} + vcpkgTriplet: ${{ matrix.config.vcpkg_triplet }} + + - name: Minetest CMake + run: | + cmake ${{matrix.config.generator}} ` + -DCMAKE_TOOLCHAIN_FILE="${{ github.workspace }}\vcpkg\scripts\buildsystems\vcpkg.cmake" ` + -DCMAKE_BUILD_TYPE=Release ` + -DENABLE_POSTGRESQL=OFF ` + -DRUN_IN_PLACE=${{ contains(matrix.type, 'portable') }} . + + - name: Build Minetest + run: cmake --build . --config Release + + - name: CPack + run: | + If ($env:TYPE -eq "installer") + { + cpack -G WIX -B "$env:GITHUB_WORKSPACE\Package" + } + ElseIf($env:TYPE -eq "portable") + { + cpack -G ZIP -B "$env:GITHUB_WORKSPACE\Package" + } + env: + TYPE: ${{matrix.type}} + + - name: Package Clean + run: rm -r $env:GITHUB_WORKSPACE\Package\_CPack_Packages + + - uses: actions/upload-artifact@v3 + with: + name: msvc-${{ matrix.config.arch }}-${{ matrix.type }} + path: .\Package\ diff --git a/.github/workflows/cpp_lint.yml b/.github/workflows/cpp_lint.yml new file mode 100644 index 0000000..581ee06 --- /dev/null +++ b/.github/workflows/cpp_lint.yml @@ -0,0 +1,55 @@ +name: cpp_lint + +# lint on c/cpp changes or workflow changes +on: + push: + paths: + - 'lib/**.[ch]' + - 'lib/**.cpp' + - 'src/**.[ch]' + - 'src/**.cpp' + - '**/CMakeLists.txt' + - 'cmake/Modules/**' + - 'util/ci/**' + - '.github/workflows/**.yml' + pull_request: + paths: + - 'lib/**.[ch]' + - 'lib/**.cpp' + - 'src/**.[ch]' + - 'src/**.cpp' + - '**/CMakeLists.txt' + - 'cmake/Modules/**' + - 'util/ci/**' + - '.github/workflows/**.yml' + +jobs: + +# clang_format: +# runs-on: ubuntu-20.04 +# steps: +# - uses: actions/checkout@v3 +# - name: Install clang-format +# run: | +# sudo apt-get update +# sudo apt-get install -y clang-format-9 +# +# - name: Run clang-format +# run: | +# source ./util/ci/clang-format.sh +# check_format +# env: +# CLANG_FORMAT: clang-format-9 + + clang_tidy: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: | + source ./util/ci/common.sh + install_linux_deps clang-tidy-9 + + - name: Run clang-tidy + run: | + ./util/ci/clang-tidy.sh diff --git a/.github/workflows/lua.yml b/.github/workflows/lua.yml new file mode 100644 index 0000000..21cbbdc --- /dev/null +++ b/.github/workflows/lua.yml @@ -0,0 +1,72 @@ +name: lua_lint + +# Lint on lua changes on builtin or if workflow changed +on: + push: + paths: + - 'builtin/**.lua' + - 'games/devtest/**.lua' + - '.github/workflows/**.yml' + pull_request: + paths: + - 'builtin/**.lua' + - 'games/devtest/**.lua' + - '.github/workflows/**.yml' + +jobs: + # Note that the integration tests are also run build.yml, but only when C++ code is changed. + integration_tests: + name: "Compile and run multiplayer tests" + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: | + source ./util/ci/common.sh + install_linux_deps clang-10 gdb libluajit-5.1-dev + + - name: Build + run: | + ./util/ci/build.sh + env: + CC: clang-10 + CXX: clang++-10 + CMAKE_FLAGS: "-DENABLE_GETTEXT=0 -DBUILD_SERVER=0" + + - name: Integration test + devtest + run: | + ./util/test_multiplayer.sh + + luacheck: + name: "Builtin Luacheck and Unit Tests" + runs-on: ubuntu-20.04 + + steps: + + - uses: actions/checkout@v3 + - uses: leafo/gh-actions-lua@v9 + with: + luaVersion: "5.1.5" + - uses: leafo/gh-actions-luarocks@v4 + + - name: Install LuaJIT + run: | + cd $HOME + git clone https://github.com/LuaJIT/LuaJIT/ + cd LuaJIT + make -j$(nproc) + + - name: Install luarocks tools + run: | + luarocks install --local luacheck + luarocks install --local busted + + - name: Run checks (builtin) + run: | + $HOME/.luarocks/bin/luacheck builtin + $HOME/.luarocks/bin/busted builtin + $HOME/.luarocks/bin/busted builtin --lua=$HOME/LuaJIT/src/luajit + + - name: Run checks (devtest) + run: | + $HOME/.luarocks/bin/luacheck --config=games/devtest/.luacheckrc games/devtest diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 0000000..edc6630 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,67 @@ +name: macos + +# build on c/cpp changes or workflow changes +on: + push: + paths: + - 'lib/**.[ch]' + - 'lib/**.cpp' + - 'src/**.[ch]' + - 'src/**.cpp' + - '**/CMakeLists.txt' + - 'cmake/Modules/**' + - '.github/workflows/macos.yml' + pull_request: + paths: + - 'lib/**.[ch]' + - 'lib/**.cpp' + - 'src/**.[ch]' + - 'src/**.cpp' + - '**/CMakeLists.txt' + - 'cmake/Modules/**' + - '.github/workflows/macos.yml' + +env: + MINETEST_GAME_REPO: https://github.com/minetest/minetest_game.git + MINETEST_GAME_BRANCH: master + MINETEST_GAME_NAME: minetest_game + +jobs: + build: + runs-on: macos-11 + steps: + - uses: actions/checkout@v3 + - name: Install deps + run: | + source ./util/ci/common.sh + install_macos_deps + + - name: Build + run: | + git clone -b $MINETEST_GAME_BRANCH $MINETEST_GAME_REPO games/$MINETEST_GAME_NAME + git clone https://github.com/minetest/irrlicht lib/irrlichtmt --depth 1 -b $(cat misc/irrlichtmt_tag.txt) + mkdir build + cd build + cmake .. \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.14 \ + -DCMAKE_FIND_FRAMEWORK=LAST \ + -DCMAKE_INSTALL_PREFIX=../build/macos/ \ + -DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE + make -j2 + make install + + - name: Test + run: | + ./build/macos/minetest.app/Contents/MacOS/minetest --run-unittests + + # Zipping the built .app preserves permissions on the contained files, + # which the GitHub artifact pipeline would otherwise strip away. + - name: CPack + run: | + cd build + cpack -G ZIP -B macos + + - uses: actions/upload-artifact@v3 + with: + name: minetest-macos + path: ./build/macos/*.zip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bb5e0a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +## Editors and development environments +*~ +*.swp +*.bak* +*.orig +.DS_Store +# Vim +*.vim +# Kate +.*.kate-swp +.swp.* +# KDevelop4 +.kdev4/ +*.kdev4 +# Eclipse (CDT and LDT) +.project +.cproject +.settings/ +.buildpath +.metadata +# GNU Global +tags +!tags/ +gtags.files +.idea +# Codelite +*.project +# Visual Studio Code & plugins +.vscode/ +build/.cmake/ +# Gradle +.gradle + +## Files related to Minetest development cycle +/*.patch +*.diff +# GNU Patch reject file +*.rej + +## Non-static Minetest directories or symlinks to these +/bin/ +/games/* +!/games/devtest/ +/cache +/textures/* +!/textures/base/ +/screenshots +/sounds +/mods/* +!/mods/minetest/ +/mods/minetest/* +!/mods/minetest/mods_here.txt +/worlds +/world/ +/clientmods/* +!/clientmods/preview/ +/client/mod_storage/ + +## Configuration/log files +minetest.conf +debug.txt +debug.txt.1 + +## Other files generated by Minetest +screenshot_*.png +testbm.txt + +## Doxygen files +doc/Doxyfile +doc/html/ +doc/doxygen_* + +## MkDocs files +public/ +doc/mkdocs/docs/*.md +doc/mkdocs/mkdocs.yml + +## Build files +build/ +CMakeFiles +Makefile +cmake_install.cmake +CMakeCache.txt +CPackConfig.cmake +CPackSourceConfig.cmake +src/test_config.h +src/cmake_config.h +src/cmake_config_githash.h +src/unittest/test_world/world.mt +games/devtest/mods/testnodes/textures/testnodes_generated_*.png +/locale/ +.directory +*.cbp +*.layout +*.o +*.a +*.ninja +.ninja* +*.gch +*.iml +test_config.h +cmake-build-debug/ +cmake-build-release/ +cmake_config.h +cmake_config_githash.h +CMakeDoxy* +compile_commands.json +*.apk +*.zip +# Visual Studio +*.vcxproj* +*.sln +.vs/ + +# Optional user provided library folder +lib/irrlichtmt + +# Generated mod storage database +client/mod_storage.sqlite diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..28e35a9 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,289 @@ +--- +# Github repository is cloned every day on Gitlab.com +# https://gitlab.com/minetest/minetest +# Pipelines URL: https://gitlab.com/minetest/minetest/pipelines + +stages: + - build + - package + - deploy + +variables: + MINETEST_GAME_REPO: "https://github.com/minetest/minetest_game.git" + CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH + +.build_template: + stage: build + before_script: + - apt-get update + - DEBIAN_FRONTEND=noninteractive apt-get -y install build-essential git cmake libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libleveldb-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev libzstd-dev + script: + - git clone https://github.com/minetest/irrlicht lib/irrlichtmt --depth 1 -b $(cat misc/irrlichtmt_tag.txt) + - mkdir build && cd build + - cmake -DCMAKE_INSTALL_PREFIX=../artifact/minetest/usr/ -DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE -DBUILD_SERVER=TRUE .. + - make -j $(($(nproc) + 1)) + - make install + artifacts: + when: on_success + expire_in: 1h + paths: + - artifact/* + +.debpkg_template: + stage: package + before_script: + - apt-get update + - apt-get install -y git + - mkdir -p build/deb/minetest/DEBIAN/ + - cp misc/debpkg-control build/deb/minetest/DEBIAN/control + - cp -a artifact/minetest/usr build/deb/minetest/ + script: + - git clone $MINETEST_GAME_REPO build/deb/minetest/usr/share/minetest/games/minetest_game + - rm -rf build/deb/minetest/usr/share/minetest/games/minetest/.git + - sed -i 's/DATEPLACEHOLDER/'$(date +%y.%m.%d)'/g' build/deb/minetest/DEBIAN/control + - sed -i 's/JPEG_PLACEHOLDER/'$JPEG_PKG'/g' build/deb/minetest/DEBIAN/control + - sed -i 's/LEVELDB_PLACEHOLDER/'$LEVELDB_PKG'/g' build/deb/minetest/DEBIAN/control + - sed -i 's/JSONCPP_PLACEHOLDER/'$JSONCPP_PKG'/g' build/deb/minetest/DEBIAN/control + - cd build/deb/ && dpkg-deb -b minetest/ && mv minetest.deb ../../ + artifacts: + expire_in: 90 day + paths: + - ./*.deb + +.debpkg_install: + stage: deploy + before_script: + - apt-get update -qy + script: + - apt-get install -y ./*.deb + - minetest --version + +## +## Debian +## + +# Stretch + +build:debian-9: + extends: .build_template + image: debian:9 + +package:debian-9: + extends: .debpkg_template + image: debian:9 + needs: + - build:debian-9 + variables: + JSONCPP_PKG: libjsoncpp1 + LEVELDB_PKG: libleveldb1v5 + JPEG_PKG: libjpeg62-turbo + +deploy:debian-9: + extends: .debpkg_install + image: debian:9 + needs: + - package:debian-9 + +# Buster + +build:debian-10: + extends: .build_template + image: debian:10 + +package:debian-10: + extends: .debpkg_template + image: debian:10 + needs: + - build:debian-10 + variables: + JSONCPP_PKG: libjsoncpp1 + LEVELDB_PKG: libleveldb1d + JPEG_PKG: libjpeg62-turbo + +deploy:debian-10: + extends: .debpkg_install + image: debian:10 + needs: + - package:debian-10 + +# Bullseye + +build:debian-11: + extends: .build_template + image: debian:11 + +package:debian-11: + extends: .debpkg_template + image: debian:11 + needs: + - build:debian-11 + variables: + JSONCPP_PKG: libjsoncpp24 + LEVELDB_PKG: libleveldb1d + JPEG_PKG: libjpeg62-turbo + +deploy:debian-11: + extends: .debpkg_install + image: debian:11 + needs: + - package:debian-11 + +## +## Ubuntu +## + +# Bionic + +build:ubuntu-18.04: + extends: .build_template + image: ubuntu:bionic + +package:ubuntu-18.04: + extends: .debpkg_template + image: ubuntu:bionic + needs: + - build:ubuntu-18.04 + variables: + JSONCPP_PKG: libjsoncpp1 + LEVELDB_PKG: libleveldb1v5 + JPEG_PKG: libjpeg-turbo8 + +deploy:ubuntu-18.04: + extends: .debpkg_install + image: ubuntu:bionic + needs: + - package:ubuntu-18.04 + +# Focal + +build:ubuntu-20.04: + extends: .build_template + image: ubuntu:focal + +package:ubuntu-20.04: + extends: .debpkg_template + image: ubuntu:focal + needs: + - build:ubuntu-20.04 + variables: + JSONCPP_PKG: libjsoncpp1 + LEVELDB_PKG: libleveldb1d + JPEG_PKG: libjpeg-turbo8 + +deploy:ubuntu-20.04: + extends: .debpkg_install + image: ubuntu:focal + needs: + - package:ubuntu-20.04 + +## +## Fedora +## + +# Fedora 28 <-> RHEL 8 +build:fedora-28: + extends: .build_template + image: fedora:28 + before_script: + - dnf -y install make git gcc gcc-c++ kernel-devel cmake libjpeg-devel libpng-devel libcurl-devel openal-soft-devel libvorbis-devel libXxf86vm-devel libogg-devel freetype-devel mesa-libGL-devel zlib-devel jsoncpp-devel gmp-devel sqlite-devel luajit-devel leveldb-devel ncurses-devel spatialindex-devel libzstd-devel + +## +## MinGW for Windows +## + +.generic_win_template: + image: ubuntu:focal + before_script: + - apt-get update + - DEBIAN_FRONTEND=noninteractive apt-get install -y wget xz-utils unzip git cmake gettext + - wget -nv http://minetest.kitsunemimi.pw/mingw-w64-${WIN_ARCH}_11.2.0_ubuntu20.04.tar.xz -O mingw.tar.xz + - tar -xaf mingw.tar.xz -C /usr + +.build_win_template: + extends: .generic_win_template + stage: build + artifacts: + expire_in: 90 day + paths: + - minetest-*-win*/* + +build:win32: + extends: .build_win_template + script: + - EXISTING_MINETEST_DIR=$PWD ./util/buildbot/buildwin32.sh build + - unzip -q build/build/*.zip + variables: + WIN_ARCH: "i686" + +build:win64: + extends: .build_win_template + script: + - EXISTING_MINETEST_DIR=$PWD ./util/buildbot/buildwin64.sh build + - unzip -q build/build/*.zip + variables: + WIN_ARCH: "x86_64" + +## +## Docker +## + +package:docker: + stage: package + image: docker:stable + services: + - docker:dind + before_script: + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com + script: + - docker build . -t ${CONTAINER_IMAGE}/server:$CI_COMMIT_SHA -t ${CONTAINER_IMAGE}/server:$CI_COMMIT_REF_NAME -t ${CONTAINER_IMAGE}/server:latest + - docker push ${CONTAINER_IMAGE}/server:$CI_COMMIT_SHA + - docker push ${CONTAINER_IMAGE}/server:$CI_COMMIT_REF_NAME + - docker push ${CONTAINER_IMAGE}/server:latest + +## +## Gitlab Pages (Lua API documentation) +## + +pages: + stage: deploy + image: python:3.8 + before_script: + - pip install git+https://github.com/Python-Markdown/markdown.git + - pip install git+https://github.com/mkdocs/mkdocs.git + - pip install pygments + script: + - cd doc/mkdocs && ./build.sh + artifacts: + paths: + - public + only: + - master + +## +## AppImage +## + +package:appimage-client: + stage: package + image: appimagecrafters/appimage-builder + needs: + - build:ubuntu-18.04 + before_script: + - apt-get update -y + - apt-get install -y git + # Collect files + - mkdir AppDir + - cp -a artifact/minetest/usr/ AppDir/usr/ + - rm AppDir/usr/bin/minetestserver + - cp -a clientmods AppDir/usr/share/minetest + script: + - git clone $MINETEST_GAME_REPO AppDir/usr/share/minetest/games/minetest_game + - rm -rf AppDir/usr/share/minetest/games/minetest/.git + - export VERSION=$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA + # Remove PrefersNonDefaultGPU property due to validation errors + - sed -i '/PrefersNonDefaultGPU/d' AppDir/usr/share/applications/net.minetest.minetest.desktop + - appimage-builder --skip-test + artifacts: + expire_in: 90 day + paths: + - ./*.AppImage diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..a922bde --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,82 @@ +unused_args = false +allow_defined_top = true + +ignore = { + "131", -- Unused global variable + "431", -- Shadowing an upvalue + "432", -- Shadowing an upvalue argument +} + +read_globals = { + "ItemStack", + "INIT", + "DIR_DELIM", + "dump", "dump2", + "fgettext", "fgettext_ne", + "vector", + "VoxelArea", + "profiler", + "Settings", + + string = {fields = {"split", "trim"}}, + table = {fields = {"copy", "getn", "indexof", "insert_all"}}, + math = {fields = {"hypot", "round"}}, +} + +globals = { + "core", + "gamedata", + os = { fields = { "tempfolder" } }, + "_", +} + +files["builtin/client/register.lua"] = { + globals = { + debug = {fields={"getinfo"}}, + } +} + +files["builtin/common/misc_helpers.lua"] = { + globals = { + "dump", "dump2", "table", "math", "string", + "fgettext", "fgettext_ne", "basic_dump", "game", -- ??? + "file_exists", "get_last_folder", "cleanup_path", -- ??? + }, +} + +files["builtin/common/vector.lua"] = { + globals = { "vector" }, +} + +files["builtin/game/voxelarea.lua"] = { + globals = { "VoxelArea" }, +} + +files["builtin/game/init.lua"] = { + globals = { "profiler" }, +} + +files["builtin/common/filterlist.lua"] = { + globals = { + "filterlist", + "compare_worlds", "sort_worlds_alphabetic", "sort_mod_list", -- ??? + }, +} + +files["builtin/mainmenu"] = { + globals = { + "gamedata", + }, + + read_globals = { + "PLATFORM", + }, +} + +files["builtin/common/tests"] = { + read_globals = { + "describe", + "it", + "assert", + }, +} diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..76190d0 --- /dev/null +++ b/.mailmap @@ -0,0 +1,69 @@ +# Documentation: https://git-scm.com/docs/git-check-mailmap#_mapping_authors + +0gb.us <0gb.us@0gb.us> +Calinou +Calinou +Perttu Ahola +Perttu Ahola celeron55 +Zeno- +Zeno- +Diego Martínez +Diego Martínez +Ilya Zhuravlev +Ilya Zhuravlev +kwolekr +PilzAdam +PilzAdam +proller +proller +RealBadAngel +RealBadAngel +Selat +ShadowNinja ShadowNinja +Esteban I. Ruiz Moreno +Esteban I. Ruiz Moreno +Lord James +BlockMen +sfan5 +DannyDark +Ilya Pavlov +sapier sapier +sapier sapier +SmallJoker +Loïc Blot +Loïc Blot +numzero Vitaliy +numzero +Jean-Patrick Guerrero +Jean-Patrick Guerrero +HybridDog <3192173+HybridDog@users.noreply.github.com> +srfqi +Dániel Juhász +rubenwardy +rubenwardy +Paul Ouellette +Vanessa Dannenberg +ClobberXD +ClobberXD +ClobberXD <36130650+ClobberXD@users.noreply.github.com> +Auke Kok +Auke Kok +Desour +Nathanaëlle Courant +Ezhh +paramat +paramat +lhofhansl +red-001 +Wuzzy +Wuzzy +Wuzzy +Jordach +MoNTE48 +v-rob +v-rob <31123645+v-rob@users.noreply.github.com> +EvidenceB <49488517+EvidenceBKidscode@users.noreply.github.com> +gregorycu +Rogier +Rogier +x2048 diff --git a/AppImageBuilder.yml b/AppImageBuilder.yml new file mode 100644 index 0000000..5788e24 --- /dev/null +++ b/AppImageBuilder.yml @@ -0,0 +1,54 @@ +version: 1 + +AppDir: + path: ./AppDir + + app_info: + id: minetest + name: Minetest + icon: minetest + version: !ENV ${VERSION} + exec: usr/bin/minetest + exec_args: $@ + runtime: + env: + APPDIR_LIBRARY_PATH: $APPDIR/usr/lib/x86_64-linux-gnu + + apt: + arch: amd64 + sources: + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main universe + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main universe + + include: + - libc6 + - libcurl3-gnutls + - libfreetype6 + - libgl1 + - libjpeg-turbo8 + - libjsoncpp1 + - libleveldb1v5 + - libopenal1 + - libpng16-16 + - libsqlite3-0 + - libstdc++6 + - libvorbisfile3 + - libx11-6 + - libxxf86vm1 + - zlib1g + + files: + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + +AppImage: + update-information: None + sign-key: None + arch: x86_64 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f19b5cd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,402 @@ +cmake_minimum_required(VERSION 3.5) + +# Set policies up to 3.9 since we want to enable the IPO option +if(${CMAKE_VERSION} VERSION_LESS 3.9) + cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION}) +else() + cmake_policy(VERSION 3.9) +endif() + +# This can be read from ${PROJECT_NAME} after project() is called +project(minetest) +set(PROJECT_NAME_CAPITALIZED "Minetest") + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED TRUE) +set(GCC_MINIMUM_VERSION "5.1") +set(CLANG_MINIMUM_VERSION "3.5") + +# Also remember to set PROTOCOL_VERSION in network/networkprotocol.h when releasing +set(VERSION_MAJOR 5) +set(VERSION_MINOR 6) +set(VERSION_PATCH 1) +set(VERSION_EXTRA "" CACHE STRING "Stuff to append to version string") + +# Change to false for releases +set(DEVELOPMENT_BUILD FALSE) + +set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") +if(VERSION_EXTRA) + set(VERSION_STRING "${VERSION_STRING}-${VERSION_EXTRA}") +elseif(DEVELOPMENT_BUILD) + set(VERSION_STRING "${VERSION_STRING}-dev") +endif() + +if (CMAKE_BUILD_TYPE STREQUAL Debug) + # Append "-debug" to version string + set(VERSION_STRING "${VERSION_STRING}-debug") +endif() + +message(STATUS "*** Will build version ${VERSION_STRING} ***") + + +# Configuration options +set(DEFAULT_RUN_IN_PLACE FALSE) +if(WIN32) + set(DEFAULT_RUN_IN_PLACE TRUE) +endif() +set(RUN_IN_PLACE ${DEFAULT_RUN_IN_PLACE} CACHE BOOL + "Run directly in source directory structure") + + +set(BUILD_CLIENT TRUE CACHE BOOL "Build client") +set(BUILD_SERVER FALSE CACHE BOOL "Build server") +set(BUILD_UNITTESTS TRUE CACHE BOOL "Build unittests") +set(BUILD_BENCHMARKS FALSE CACHE BOOL "Build benchmarks") + +set(WARN_ALL TRUE CACHE BOOL "Enable -Wall for Release build") + +if(NOT CMAKE_BUILD_TYPE) + # Default to release + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type: Debug or Release" FORCE) +endif() + +set(ENABLE_UPDATE_CHECKER (NOT ${DEVELOPMENT_BUILD}) CACHE BOOL + "Whether to enable update checks by default") + +# Included stuff +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") + + +set(IRRLICHTMT_BUILD_DIR "" CACHE PATH "Path to IrrlichtMt build directory.") +if(NOT "${IRRLICHTMT_BUILD_DIR}" STREQUAL "") + find_package(IrrlichtMt QUIET + PATHS "${IRRLICHTMT_BUILD_DIR}" + NO_DEFAULT_PATH + ) + + if(NOT TARGET IrrlichtMt::IrrlichtMt) + # find_package() searches certain subdirectories. ${PATH}/cmake is not + # the only one, but it is the one where IrrlichtMt is supposed to export + # IrrlichtMtConfig.cmake + message(FATAL_ERROR "Could not find IrrlichtMtConfig.cmake in ${IRRLICHTMT_BUILD_DIR}/cmake.") + endif() +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/lib/irrlichtmt") + message(STATUS "Using user-provided IrrlichtMt at subdirectory 'lib/irrlichtmt'") + if(BUILD_CLIENT) + # tell IrrlichtMt to create a static library + set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build shared library" FORCE) + add_subdirectory(lib/irrlichtmt EXCLUDE_FROM_ALL) + unset(BUILD_SHARED_LIBS CACHE) + + if(NOT TARGET IrrlichtMt) + message(FATAL_ERROR "IrrlichtMt project is missing a CMake target?!") + endif() + else() + add_library(IrrlichtMt::IrrlichtMt INTERFACE IMPORTED) + set_target_properties(IrrlichtMt::IrrlichtMt PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/lib/irrlichtmt/include") + endif() +else() + find_package(IrrlichtMt QUIET) + if(NOT TARGET IrrlichtMt::IrrlichtMt) + string(CONCAT explanation_msg + "The Minetest team has forked Irrlicht to make their own customizations. " + "It can be found here: https://github.com/minetest/irrlicht\n" + "For example use: git clone --depth=1 https://github.com/minetest/irrlicht lib/irrlichtmt\n") + if(BUILD_CLIENT) + message(FATAL_ERROR "IrrlichtMt is required to build the client, but it was not found.\n${explanation_msg}") + endif() + + include(MinetestFindIrrlichtHeaders) + if(NOT IRRLICHT_INCLUDE_DIR) + message(FATAL_ERROR "IrrlichtMt headers are required to build the server, but none found.\n${explanation_msg}") + endif() + message(STATUS "Found IrrlichtMt headers: ${IRRLICHT_INCLUDE_DIR}") + add_library(IrrlichtMt::IrrlichtMt INTERFACE IMPORTED) + # Note that we can't use target_include_directories() since that doesn't work for IMPORTED targets before CMake 3.11 + set_target_properties(IrrlichtMt::IrrlichtMt PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${IRRLICHT_INCLUDE_DIR}") + endif() +endif() + +if(BUILD_CLIENT AND TARGET IrrlichtMt::IrrlichtMt) + # retrieve version somehow + if(NOT IrrlichtMt_VERSION) + get_target_property(IrrlichtMt_VERSION IrrlichtMt VERSION) + endif() + message(STATUS "Found IrrlichtMt ${IrrlichtMt_VERSION}") + + set(TARGET_VER_S 1.9.0mt8) + string(REPLACE "mt" "." TARGET_VER ${TARGET_VER_S}) + if(IrrlichtMt_VERSION VERSION_LESS ${TARGET_VER}) + message(FATAL_ERROR "At least IrrlichtMt ${TARGET_VER_S} is required to build") + elseif(NOT DEVELOPMENT_BUILD AND IrrlichtMt_VERSION VERSION_GREATER ${TARGET_VER}) + message(FATAL_ERROR "IrrlichtMt ${TARGET_VER_S} is required to build") + endif() +endif() + + +# Installation + +if(WIN32) + set(SHAREDIR ".") + set(BINDIR "bin") + set(DOCDIR "doc") + set(EXAMPLE_CONF_DIR ".") + set(LOCALEDIR "locale") +elseif(APPLE) + set(BUNDLE_NAME ${PROJECT_NAME}.app) + set(BUNDLE_PATH "${BUNDLE_NAME}") + set(BINDIR ${BUNDLE_NAME}/Contents/MacOS) + set(SHAREDIR ${BUNDLE_NAME}/Contents/Resources) + set(DOCDIR "${SHAREDIR}/${PROJECT_NAME}") + set(EXAMPLE_CONF_DIR ${DOCDIR}) + set(LOCALEDIR "${SHAREDIR}/locale") +elseif(UNIX) # Linux, BSD etc + if(RUN_IN_PLACE) + set(SHAREDIR ".") + set(BINDIR "bin") + set(DOCDIR "doc") + set(EXAMPLE_CONF_DIR ".") + set(MANDIR "unix/man") + set(XDG_APPS_DIR "unix/applications") + set(APPDATADIR "unix/metainfo") + set(ICONDIR "unix/icons") + set(LOCALEDIR "locale") + else() + include(GNUInstallDirs) + set(SHAREDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}") + set(BINDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}") + set(DOCDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DOCDIR}") + set(MANDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_MANDIR}") + set(EXAMPLE_CONF_DIR ${DOCDIR}) + set(XDG_APPS_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/applications") + set(APPDATADIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/metainfo") + set(ICONDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/icons") + set(LOCALEDIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LOCALEDIR}") + endif() +endif() + +set(CUSTOM_SHAREDIR "" CACHE STRING "Directory to install data files into") +if(NOT CUSTOM_SHAREDIR STREQUAL "") + set(SHAREDIR "${CUSTOM_SHAREDIR}") + message(STATUS "Using SHAREDIR=${SHAREDIR}") +endif() + +set(CUSTOM_BINDIR "" CACHE STRING "Directory to install binaries into") +if(NOT CUSTOM_BINDIR STREQUAL "") + set(BINDIR "${CUSTOM_BINDIR}") + message(STATUS "Using BINDIR=${BINDIR}") +endif() + +set(CUSTOM_DOCDIR "" CACHE STRING "Directory to install documentation into") +if(NOT CUSTOM_DOCDIR STREQUAL "") + set(DOCDIR "${CUSTOM_DOCDIR}") + if(NOT RUN_IN_PLACE) + set(EXAMPLE_CONF_DIR ${DOCDIR}) + endif() + message(STATUS "Using DOCDIR=${DOCDIR}") +endif() + +set(CUSTOM_MANDIR "" CACHE STRING "Directory to install manpages into") +if(NOT CUSTOM_MANDIR STREQUAL "") + set(MANDIR "${CUSTOM_MANDIR}") + message(STATUS "Using MANDIR=${MANDIR}") +endif() + +set(CUSTOM_EXAMPLE_CONF_DIR "" CACHE STRING "Directory to install example config file into") +if(NOT CUSTOM_EXAMPLE_CONF_DIR STREQUAL "") + set(EXAMPLE_CONF_DIR "${CUSTOM_EXAMPLE_CONF_DIR}") + message(STATUS "Using EXAMPLE_CONF_DIR=${EXAMPLE_CONF_DIR}") +endif() + +set(CUSTOM_XDG_APPS_DIR "" CACHE STRING "Directory to install .desktop files into") +if(NOT CUSTOM_XDG_APPS_DIR STREQUAL "") + set(XDG_APPS_DIR "${CUSTOM_XDG_APPS_DIR}") + message(STATUS "Using XDG_APPS_DIR=${XDG_APPS_DIR}") +endif() + +set(CUSTOM_ICONDIR "" CACHE STRING "Directory to install icons into") +if(NOT CUSTOM_ICONDIR STREQUAL "") + set(ICONDIR "${CUSTOM_ICONDIR}") + message(STATUS "Using ICONDIR=${ICONDIR}") +endif() + +set(CUSTOM_LOCALEDIR "" CACHE STRING "Directory to install l10n files into") +if(NOT CUSTOM_LOCALEDIR STREQUAL "") + set(LOCALEDIR "${CUSTOM_LOCALEDIR}") + message(STATUS "Using LOCALEDIR=${LOCALEDIR}") +endif() + + +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/builtin" DESTINATION "${SHAREDIR}") +if(RUN_IN_PLACE) + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/mods/mods_here.txt" DESTINATION "${SHAREDIR}/mods") + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/textures/texture_packs_here.txt" DESTINATION "${SHAREDIR}/textures") +endif() + +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/games/minetest_game" DESTINATION "${SHAREDIR}/games/" + COMPONENT "SUBGAME_MINETEST_GAME" OPTIONAL PATTERN ".git*" EXCLUDE ) +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/games/devtest" DESTINATION "${SHAREDIR}/games/" + COMPONENT "SUBGAME_MINIMAL" OPTIONAL PATTERN ".git*" EXCLUDE ) + +if(BUILD_CLIENT) + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/client/shaders" DESTINATION "${SHAREDIR}/client") + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/textures/base/pack" DESTINATION "${SHAREDIR}/textures/base") + if(RUN_IN_PLACE) + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/clientmods" DESTINATION "${SHAREDIR}") + install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/client/serverlist" DESTINATION "${SHAREDIR}/client") + endif() +endif() + +install(FILES "README.md" DESTINATION "${DOCDIR}" COMPONENT "Docs") +install(FILES "doc/lua_api.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs") +install(FILES "doc/client_lua_api.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs") +install(FILES "doc/menu_lua_api.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs") +install(FILES "doc/texture_packs.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs") +install(FILES "doc/world_format.txt" DESTINATION "${DOCDIR}" COMPONENT "Docs") +install(FILES "minetest.conf.example" DESTINATION "${EXAMPLE_CONF_DIR}") + +if(UNIX AND NOT APPLE) + install(FILES "doc/minetest.6" "doc/minetestserver.6" DESTINATION "${MANDIR}/man6") + install(FILES "misc/net.minetest.minetest.desktop" DESTINATION "${XDG_APPS_DIR}") + install(FILES "misc/net.minetest.minetest.appdata.xml" DESTINATION "${APPDATADIR}") + install(FILES "misc/minetest.svg" DESTINATION "${ICONDIR}/hicolor/scalable/apps") + install(FILES "misc/minetest-xorg-icon-128.png" + DESTINATION "${ICONDIR}/hicolor/128x128/apps" + RENAME "minetest.png") +endif() + +if(APPLE) + install(FILES "misc/minetest-icon.icns" DESTINATION "${SHAREDIR}") + install(FILES "misc/Info.plist" DESTINATION "${BUNDLE_PATH}/Contents") +endif() + +# Library pack +find_package(GMP REQUIRED) +find_package(Json REQUIRED) +find_package(Lua REQUIRED) +if(NOT USE_LUAJIT) + set(LUA_BIT_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lib/bitop) + set(LUA_BIT_LIBRARY bitop) + add_subdirectory(lib/bitop) +endif() + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "${GCC_MINIMUM_VERSION}") + message(FATAL_ERROR "Insufficient gcc version, found ${CMAKE_CXX_COMPILER_VERSION}. " + "Version ${GCC_MINIMUM_VERSION} or higher is required.") + endif() +elseif(CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?Clang") + if (CMAKE_CXX_COMPILER_VERSION VERSION_LESS "${CLANG_MINIMUM_VERSION}") + message(FATAL_ERROR "Insufficient clang version, found ${CMAKE_CXX_COMPILER_VERSION}. " + "Version ${CLANG_MINIMUM_VERSION} or higher is required.") + endif() +endif() + +if(BUILD_BENCHMARKS) + add_subdirectory(lib/catch2) +endif() + +# Subdirectories +# Be sure to add all relevant definitions above this +add_subdirectory(src) + + +# CPack + +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A free open-source voxel game engine with easy modding and game creation.") +set(CPACK_PACKAGE_VERSION_MAJOR ${VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${VERSION_PATCH}) +set(CPACK_PACKAGE_VENDOR "celeron55") +set(CPACK_PACKAGE_CONTACT "Perttu Ahola ") + +include(CPackComponent) + +cpack_add_component(Docs + DISPLAY_NAME "Documentation" + DESCRIPTION "Documentation about Minetest and Minetest modding" +) + +cpack_add_component(SUBGAME_MINETEST_GAME + DISPLAY_NAME "Minetest Game" + DESCRIPTION "The default game bundled in the Minetest engine. Mainly used as a modding base." + GROUP "Games" +) + +cpack_add_component(SUBGAME_MINIMAL + DISPLAY_NAME "Development Test" + DESCRIPTION "A basic testing environment used for engine development and sometimes for testing mods." + DISABLED #DISABLED does not mean it is disabled, and is just not selected by default. + GROUP "Games" +) + +cpack_add_component_group(Subgames + DESCRIPTION "Games for the Minetest engine." +) + +if(WIN32) + # Include all dynamically linked runtime libaries such as MSVCRxxx.dll + include(InstallRequiredSystemLibraries) + + if(RUN_IN_PLACE) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${VERSION_STRING}-win64") + else() + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${VERSION_STRING}-win32") + endif() + + set(CPACK_GENERATOR ZIP) + + else() + set(CPACK_GENERATOR WIX) + set(CPACK_PACKAGE_NAME "${PROJECT_NAME_CAPITALIZED}") + set(CPACK_PACKAGE_INSTALL_DIRECTORY ".") + set(CPACK_PACKAGE_EXECUTABLES ${PROJECT_NAME} "${PROJECT_NAME_CAPITALIZED}") + set(CPACK_CREATE_DESKTOP_LINKS ${PROJECT_NAME}) + set(CPACK_PACKAGING_INSTALL_PREFIX "/${PROJECT_NAME_CAPITALIZED}") + + set(CPACK_WIX_PRODUCT_ICON "${CMAKE_CURRENT_SOURCE_DIR}/misc/minetest-icon.ico") + # Supported languages can be found at + # http://wixtoolset.org/documentation/manual/v3/wixui/wixui_localization.html + #set(CPACK_WIX_CULTURES "ar-SA,bg-BG,ca-ES,hr-HR,cs-CZ,da-DK,nl-NL,en-US,et-EE,fi-FI,fr-FR,de-DE") + set(CPACK_WIX_UI_BANNER "${CMAKE_CURRENT_SOURCE_DIR}/misc/CPACK_WIX_UI_BANNER.BMP") + set(CPACK_WIX_UI_DIALOG "${CMAKE_CURRENT_SOURCE_DIR}/misc/CPACK_WIX_UI_DIALOG.BMP") + + set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/doc/lgpl-2.1.txt") + + # The correct way would be to include both x32 and x64 into one installer + # and install the appropriate one. + # CMake does not support that, so there are two separate GUID's + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(CPACK_WIX_UPGRADE_GUID "745A0FB3-5552-44CA-A587-A91C397CCC56") + else() + set(CPACK_WIX_UPGRADE_GUID "814A2E2D-2779-4BBD-9ACD-FC3BD51FBBA2") + endif() + endif() +elseif(APPLE) + set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY 0) + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${VERSION_STRING}-osx") + set(CPACK_GENERATOR ZIP) +else() + set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${VERSION_STRING}-linux") + set(CPACK_GENERATOR TGZ) + set(CPACK_SOURCE_GENERATOR TGZ) +endif() + +include(CPack) + + +# Add a target to generate API documentation with Doxygen +find_package(Doxygen) +if(DOXYGEN_FOUND) + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/doc/Doxyfile.in + ${CMAKE_CURRENT_BINARY_DIR}/doc/Doxyfile @ONLY) + add_custom_target(doc + ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/doc/Doxyfile + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc + COMMENT "Generating API documentation with Doxygen" VERBATIM + ) +endif() diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3dd82e7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,71 @@ +ARG DOCKER_IMAGE=alpine:3.14 +FROM $DOCKER_IMAGE AS builder + +ENV MINETEST_GAME_VERSION master +ENV IRRLICHT_VERSION master + +COPY .git /usr/src/minetest/.git +COPY CMakeLists.txt /usr/src/minetest/CMakeLists.txt +COPY README.md /usr/src/minetest/README.md +COPY minetest.conf.example /usr/src/minetest/minetest.conf.example +COPY builtin /usr/src/minetest/builtin +COPY cmake /usr/src/minetest/cmake +COPY doc /usr/src/minetest/doc +COPY fonts /usr/src/minetest/fonts +COPY lib /usr/src/minetest/lib +COPY misc /usr/src/minetest/misc +COPY po /usr/src/minetest/po +COPY src /usr/src/minetest/src +COPY textures /usr/src/minetest/textures + +WORKDIR /usr/src/minetest + +RUN apk add --no-cache git build-base cmake sqlite-dev curl-dev zlib-dev zstd-dev \ + gmp-dev jsoncpp-dev postgresql-dev ninja luajit-dev ca-certificates && \ + git clone --depth=1 -b ${MINETEST_GAME_VERSION} https://github.com/minetest/minetest_game.git ./games/minetest_game && \ + rm -fr ./games/minetest_game/.git + +WORKDIR /usr/src/ +RUN git clone --recursive https://github.com/jupp0r/prometheus-cpp/ && \ + cd prometheus-cpp && \ + cmake -B build \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_BUILD_TYPE=Release \ + -DENABLE_TESTING=0 \ + -GNinja && \ + cmake --build build && \ + cmake --install build + +RUN git clone --depth=1 https://github.com/minetest/irrlicht/ -b ${IRRLICHT_VERSION} && \ + cp -r irrlicht/include /usr/include/irrlichtmt + +WORKDIR /usr/src/minetest +RUN cmake -B build \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SERVER=TRUE \ + -DENABLE_PROMETHEUS=TRUE \ + -DBUILD_UNITTESTS=FALSE \ + -DBUILD_CLIENT=FALSE \ + -GNinja && \ + cmake --build build && \ + cmake --install build + +ARG DOCKER_IMAGE=alpine:3.14 +FROM $DOCKER_IMAGE AS runtime + +RUN apk add --no-cache sqlite-libs curl gmp libstdc++ libgcc libpq luajit jsoncpp zstd-libs && \ + adduser -D minetest --uid 30000 -h /var/lib/minetest && \ + chown -R minetest:minetest /var/lib/minetest + +WORKDIR /var/lib/minetest + +COPY --from=builder /usr/local/share/minetest /usr/local/share/minetest +COPY --from=builder /usr/local/bin/minetestserver /usr/local/bin/minetestserver +COPY --from=builder /usr/local/share/doc/minetest/minetest.conf.example /etc/minetest/minetest.conf + +USER minetest:minetest + +EXPOSE 30000/udp 30000/tcp + +CMD ["/usr/local/bin/minetestserver", "--config", "/etc/minetest/minetest.conf"] diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1f2c6c3 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,206 @@ + +License of Minetest textures and sounds +--------------------------------------- + +This applies to textures and sounds contained in the main Minetest +distribution. + +Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) +http://creativecommons.org/licenses/by-sa/3.0/ + +textures/base/pack/refresh.png is under the Apache 2 license +https://www.apache.org/licenses/LICENSE-2.0.html + +Textures by Zughy are under CC BY-SA 4.0 +https://creativecommons.org/licenses/by-sa/4.0/ + +textures/base/pack/server_public.png is under CC-BY 4.0, taken from Twitter's Twemoji set +https://creativecommons.org/licenses/by/4.0/ + +Authors of media files +----------------------- +Everything not listed in here: +Copyright (C) 2010-2012 celeron55, Perttu Ahola + +ShadowNinja: + textures/base/pack/smoke_puff.png + +paramat: + textures/base/pack/menu_header.png + textures/base/pack/next_icon.png + textures/base/pack/prev_icon.png + textures/base/pack/clear.png + textures/base/pack/search.png + +rubenwardy, paramat: + textures/base/pack/start_icon.png + textures/base/pack/end_icon.png + +erlehmann: + misc/minetest-icon-24x24.png + misc/minetest-icon.ico + misc/minetest.svg + textures/base/pack/logo.png + +JRottm: + textures/base/pack/player_marker.png + +srifqi: + textures/base/pack/chat_hide_btn.png + textures/base/pack/chat_show_btn.png + textures/base/pack/joystick_bg.png + textures/base/pack/joystick_center.png + textures/base/pack/joystick_off.png + textures/base/pack/minimap_btn.png + +Zughy: + textures/base/pack/cdb_add.png + textures/base/pack/cdb_clear.png + textures/base/pack/cdb_downloading.png + textures/base/pack/cdb_queued.png + textures/base/pack/cdb_update.png + textures/base/pack/cdb_viewonline.png + +appgurueu: + textures/base/pack/server_incompatible.png + +erlehmann, Warr1024, rollerozxa: + textures/base/pack/no_screenshot.png + +kilbith: + textures/base/pack/server_favorite.png + +SmallJoker + textures/base/pack/server_favorite_delete.png (based on server_favorite.png) + +License of Minetest source code +------------------------------- + +Minetest +Copyright (C) 2010-2018 celeron55, Perttu Ahola + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Irrlicht +--------------- + +This program uses IrrlichtMt, Minetest's fork of +the Irrlicht Engine. http://irrlicht.sourceforge.net/ + + The Irrlicht Engine License + +Copyright © 2002-2005 Nikolaus Gebhardt + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute +it freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you use + this software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + 3. This notice may not be removed or altered from any source + distribution. + + +JThread +--------------- + +This program uses the JThread library. License for JThread follows: + +Copyright (c) 2000-2006 Jori Liesenborgs (jori.liesenborgs@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. + +Lua +--------------- + +Lua is licensed under the terms of the MIT license reproduced below. +This means that Lua is free software and can be used for both academic +and commercial purposes at absolutely no cost. + +For details and rationale, see https://www.lua.org/license.html . + +Copyright (C) 1994-2008 Lua.org, PUC-Rio. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Fonts +--------------- + +Bitstream Vera Fonts Copyright: + + Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is + a trademark of Bitstream, Inc. + +Arimo - Apache License, version 2.0 + Digitized data copyright (c) 2010-2012 Google Corporation. + +Cousine - Apache License, version 2.0 + Digitized data copyright (c) 2010-2012 Google Corporation. + +DroidSansFallBackFull: + + Copyright (C) 2008 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0bc5d4b --- /dev/null +++ b/README.md @@ -0,0 +1,480 @@ +Minetest +======== + +![Build Status](https://github.com/minetest/minetest/workflows/build/badge.svg) +[![Translation status](https://hosted.weblate.org/widgets/minetest/-/svg-badge.svg)](https://hosted.weblate.org/engage/minetest/?utm_source=widget) +[![License](https://img.shields.io/badge/license-LGPLv2.1%2B-blue.svg)](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) + +Minetest is a free open-source voxel game engine with easy modding and game creation. + +Copyright (C) 2010-2022 Perttu Ahola +and contributors (see source file comments and the version control log) + +In case you downloaded the source code +-------------------------------------- +If you downloaded the Minetest Engine source code in which this file is +contained, you probably want to download the [Minetest Game](https://github.com/minetest/minetest_game/) +project too. See its README.txt for more information. + +Table of Contents +------------------ + +1. [Further Documentation](#further-documentation) +2. [Default Controls](#default-controls) +3. [Paths](#paths) +4. [Configuration File](#configuration-file) +5. [Command-line Options](#command-line-options) +6. [Compiling](#compiling) +7. [Docker](#docker) +8. [Version Scheme](#version-scheme) + + +Further documentation +---------------------- +- Website: https://minetest.net/ +- Wiki: https://wiki.minetest.net/ +- Developer wiki: https://dev.minetest.net/ +- Forum: https://forum.minetest.net/ +- GitHub: https://github.com/minetest/minetest/ +- [doc/](doc/) directory of source distribution + +Default controls +---------------- +All controls are re-bindable using settings. +Some can be changed in the key config dialog in the settings tab. + +| Button | Action | +|-------------------------------|----------------------------------------------------------------| +| Move mouse | Look around | +| W, A, S, D | Move | +| Space | Jump/move up | +| Shift | Sneak/move down | +| Q | Drop itemstack | +| Shift + Q | Drop single item | +| Left mouse button | Dig/punch/take item | +| Right mouse button | Place/use | +| Shift + right mouse button | Build (without using) | +| I | Inventory menu | +| Mouse wheel | Select item | +| 0-9 | Select item | +| Z | Zoom (needs zoom privilege) | +| T | Chat | +| / | Command | +| Esc | Pause menu/abort/exit (pauses only singleplayer game) | +| R | Enable/disable full range view | +| + | Increase view range | +| - | Decrease view range | +| K | Enable/disable fly mode (needs fly privilege) | +| P | Enable/disable pitch move mode | +| J | Enable/disable fast mode (needs fast privilege) | +| H | Enable/disable noclip mode (needs noclip privilege) | +| E | Aux1 (Move fast in fast mode. Games may add special features) | +| C | Cycle through camera modes | +| V | Cycle through minimap modes | +| Shift + V | Change minimap orientation | +| F1 | Hide/show HUD | +| F2 | Hide/show chat | +| F3 | Disable/enable fog | +| F4 | Disable/enable camera update (Mapblocks are not updated anymore when disabled, disabled in release builds) | +| F5 | Cycle through debug information screens | +| F6 | Cycle through profiler info screens | +| F10 | Show/hide console | +| F12 | Take screenshot | + +Paths +----- +Locations: + +* `bin` - Compiled binaries +* `share` - Distributed read-only data +* `user` - User-created modifiable data + +Where each location is on each platform: + +* Windows .zip / RUN_IN_PLACE source: + * `bin` = `bin` + * `share` = `.` + * `user` = `.` +* Windows installed: + * `bin` = `C:\Program Files\Minetest\bin (Depends on the install location)` + * `share` = `C:\Program Files\Minetest (Depends on the install location)` + * `user` = `%APPDATA%\Minetest` +* Linux installed: + * `bin` = `/usr/bin` + * `share` = `/usr/share/minetest` + * `user` = `~/.minetest` +* macOS: + * `bin` = `Contents/MacOS` + * `share` = `Contents/Resources` + * `user` = `Contents/User OR ~/Library/Application Support/minetest` + +Worlds can be found as separate folders in: `user/worlds/` + +Configuration file +------------------ +- Default location: + `user/minetest.conf` +- This file is created by closing Minetest for the first time. +- A specific file can be specified on the command line: + `--config ` +- A run-in-place build will look for the configuration file in + `location_of_exe/../minetest.conf` and also `location_of_exe/../../minetest.conf` + +Command-line options +-------------------- +- Use `--help` + +Compiling +--------- +### Compiling on GNU/Linux + +#### Dependencies + +| Dependency | Version | Commentary | +|------------|---------|------------| +| GCC | 5.1+ | or Clang 3.5+ | +| CMake | 3.5+ | | +| IrrlichtMt | - | Custom version of Irrlicht, see https://github.com/minetest/irrlicht | +| Freetype | 2.0+ | | +| SQLite3 | 3+ | | +| Zstd | 1.0+ | | +| LuaJIT | 2.0+ | Bundled Lua 5.1 is used if not present | +| GMP | 5.0.0+ | Bundled mini-GMP is used if not present | +| JsonCPP | 1.0.0+ | Bundled JsonCPP is used if not present | + +For Debian/Ubuntu users: + + sudo apt install g++ make libc6-dev cmake libpng-dev libjpeg-dev libxi-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev libjsoncpp-dev libzstd-dev libluajit-5.1-dev + +For Fedora users: + + sudo dnf install make automake gcc gcc-c++ kernel-devel cmake libcurl-devel openal-soft-devel libvorbis-devel libXi-devel libogg-devel freetype-devel mesa-libGL-devel zlib-devel jsoncpp-devel gmp-devel sqlite-devel luajit-devel leveldb-devel ncurses-devel spatialindex-devel libzstd-devel + +For Arch users: + + sudo pacman -S base-devel libcurl-gnutls cmake libxi libpng sqlite libogg libvorbis openal freetype2 jsoncpp gmp luajit leveldb ncurses zstd + +For Alpine users: + + sudo apk add build-base cmake libpng-dev jpeg-dev libxi-dev mesa-dev sqlite-dev libogg-dev libvorbis-dev openal-soft-dev curl-dev freetype-dev zlib-dev gmp-dev jsoncpp-dev luajit-dev zstd-dev + +#### Download + +You can install Git for easily keeping your copy up to date. +If you don’t want Git, read below on how to get the source without Git. +This is an example for installing Git on Debian/Ubuntu: + + sudo apt install git + +For Fedora users: + + sudo dnf install git + +Download source (this is the URL to the latest of source repository, which might not work at all times) using Git: + + git clone --depth 1 https://github.com/minetest/minetest.git + cd minetest + +Download minetest_game (otherwise only the "Development Test" game is available) using Git: + + git clone --depth 1 https://github.com/minetest/minetest_game.git games/minetest_game + +Download IrrlichtMt to `lib/irrlichtmt`, it will be used to satisfy the IrrlichtMt dependency that way: + + git clone --depth 1 https://github.com/minetest/irrlicht.git lib/irrlichtmt + +Download source, without using Git: + + wget https://github.com/minetest/minetest/archive/master.tar.gz + tar xf master.tar.gz + cd minetest-master + +Download minetest_game, without using Git: + + cd games/ + wget https://github.com/minetest/minetest_game/archive/master.tar.gz + tar xf master.tar.gz + mv minetest_game-master minetest_game + cd .. + +Download IrrlichtMt, without using Git: + + cd lib/ + wget https://github.com/minetest/irrlicht/archive/master.tar.gz + tar xf master.tar.gz + mv irrlicht-master irrlichtmt + cd .. + +#### Build + +Build a version that runs directly from the source directory: + + cmake . -DRUN_IN_PLACE=TRUE + make -j$(nproc) + +Run it: + + ./bin/minetest + +- Use `cmake . -LH` to see all CMake options and their current state. +- If you want to install it system-wide (or are making a distribution package), + you will want to use `-DRUN_IN_PLACE=FALSE`. +- You can build a bare server by specifying `-DBUILD_SERVER=TRUE`. +- You can disable the client build by specifying `-DBUILD_CLIENT=FALSE`. +- You can select between Release and Debug build by `-DCMAKE_BUILD_TYPE=`. + - Debug build is slower, but gives much more useful output in a debugger. +- If you build a bare server you don't need to compile IrrlichtMt, just the headers suffice. + - In that case use `-DIRRLICHT_INCLUDE_DIR=/some/where/irrlichtmt/include`. + +- Minetest will use the IrrlichtMt package that is found first, given by the following order: + 1. Specified `IRRLICHTMT_BUILD_DIR` CMake variable + 2. `${PROJECT_SOURCE_DIR}/lib/irrlichtmt` (if existent) + 3. Installation of IrrlichtMt in the system-specific library paths + 4. For server builds with disabled `BUILD_CLIENT` variable, the headers from `IRRLICHT_INCLUDE_DIR` will be used. + - NOTE: Changing the IrrlichtMt build directory (includes system installs) requires regenerating the CMake cache (`rm CMakeCache.txt`) + +### CMake options + +General options and their default values: + + BUILD_CLIENT=TRUE - Build Minetest client + BUILD_SERVER=FALSE - Build Minetest server + BUILD_UNITTESTS=TRUE - Build unittest sources + BUILD_BENCHMARKS=FALSE - Build benchmark sources + CMAKE_BUILD_TYPE=Release - Type of build (Release vs. Debug) + Release - Release build + Debug - Debug build + SemiDebug - Partially optimized debug build + RelWithDebInfo - Release build with debug information + MinSizeRel - Release build with -Os passed to compiler to make executable as small as possible + ENABLE_CURL=ON - Build with cURL; Enables use of online mod repo, public serverlist and remote media fetching via http + ENABLE_CURSES=ON - Build with (n)curses; Enables a server side terminal (command line option: --terminal) + ENABLE_GETTEXT=ON - Build with Gettext; Allows using translations + ENABLE_GLES=OFF - Enable extra support code for OpenGL ES (requires support by IrrlichtMt) + ENABLE_LEVELDB=ON - Build with LevelDB; Enables use of LevelDB map backend + ENABLE_POSTGRESQL=ON - Build with libpq; Enables use of PostgreSQL map backend (PostgreSQL 9.5 or greater recommended) + ENABLE_REDIS=ON - Build with libhiredis; Enables use of Redis map backend + ENABLE_SPATIAL=ON - Build with LibSpatial; Speeds up AreaStores + ENABLE_SOUND=ON - Build with OpenAL, libogg & libvorbis; in-game sounds + ENABLE_LUAJIT=ON - Build with LuaJIT (much faster than non-JIT Lua) + ENABLE_PROMETHEUS=OFF - Build with Prometheus metrics exporter (listens on tcp/30000 by default) + ENABLE_SYSTEM_GMP=ON - Use GMP from system (much faster than bundled mini-gmp) + ENABLE_SYSTEM_JSONCPP=ON - Use JsonCPP from system + RUN_IN_PLACE=FALSE - Create a portable install (worlds, settings etc. in current directory) + ENABLE_UPDATE_CHECKER=TRUE - Whether to enable update checks by default + USE_GPROF=FALSE - Enable profiling using GProf + VERSION_EXTRA= - Text to append to version (e.g. VERSION_EXTRA=foobar -> Minetest 0.4.9-foobar) + ENABLE_TOUCH=FALSE - Enable Touchscreen support (requires support by IrrlichtMt) + +Library specific options: + + CURL_DLL - Only if building with cURL on Windows; path to libcurl.dll + CURL_INCLUDE_DIR - Only if building with cURL; directory where curl.h is located + CURL_LIBRARY - Only if building with cURL; path to libcurl.a/libcurl.so/libcurl.lib + EGL_INCLUDE_DIR - Only if building with GLES; directory that contains egl.h + EGL_LIBRARY - Only if building with GLES; path to libEGL.a/libEGL.so + EXTRA_DLL - Only on Windows; optional paths to additional DLLs that should be packaged + FREETYPE_INCLUDE_DIR_freetype2 - Directory that contains files such as ftimage.h + FREETYPE_INCLUDE_DIR_ft2build - Directory that contains ft2build.h + FREETYPE_LIBRARY - Path to libfreetype.a/libfreetype.so/freetype.lib + FREETYPE_DLL - Only on Windows; path to libfreetype-6.dll + GETTEXT_DLL - Only when building with gettext on Windows; paths to libintl + libiconv DLLs + GETTEXT_INCLUDE_DIR - Only when building with gettext; directory that contains iconv.h + GETTEXT_LIBRARY - Only when building with gettext on Windows; path to libintl.dll.a + GETTEXT_MSGFMT - Only when building with gettext; path to msgfmt/msgfmt.exe + IRRLICHT_DLL - Only on Windows; path to IrrlichtMt.dll + IRRLICHT_INCLUDE_DIR - Directory that contains IrrCompileConfig.h (usable for server build only) + LEVELDB_INCLUDE_DIR - Only when building with LevelDB; directory that contains db.h + LEVELDB_LIBRARY - Only when building with LevelDB; path to libleveldb.a/libleveldb.so/libleveldb.dll.a + LEVELDB_DLL - Only when building with LevelDB on Windows; path to libleveldb.dll + PostgreSQL_INCLUDE_DIR - Only when building with PostgreSQL; directory that contains libpq-fe.h + PostgreSQL_LIBRARY - Only when building with PostgreSQL; path to libpq.a/libpq.so/libpq.lib + REDIS_INCLUDE_DIR - Only when building with Redis; directory that contains hiredis.h + REDIS_LIBRARY - Only when building with Redis; path to libhiredis.a/libhiredis.so + SPATIAL_INCLUDE_DIR - Only when building with LibSpatial; directory that contains spatialindex/SpatialIndex.h + SPATIAL_LIBRARY - Only when building with LibSpatial; path to libspatialindex_c.so/spatialindex-32.lib + LUA_INCLUDE_DIR - Only if you want to use LuaJIT; directory where luajit.h is located + LUA_LIBRARY - Only if you want to use LuaJIT; path to libluajit.a/libluajit.so + OGG_DLL - Only if building with sound on Windows; path to libogg.dll + OGG_INCLUDE_DIR - Only if building with sound; directory that contains an ogg directory which contains ogg.h + OGG_LIBRARY - Only if building with sound; path to libogg.a/libogg.so/libogg.dll.a + OPENAL_DLL - Only if building with sound on Windows; path to OpenAL32.dll + OPENAL_INCLUDE_DIR - Only if building with sound; directory where al.h is located + OPENAL_LIBRARY - Only if building with sound; path to libopenal.a/libopenal.so/OpenAL32.lib + SQLITE3_INCLUDE_DIR - Directory that contains sqlite3.h + SQLITE3_LIBRARY - Path to libsqlite3.a/libsqlite3.so/sqlite3.lib + VORBISFILE_LIBRARY - Only if building with sound; path to libvorbisfile.a/libvorbisfile.so/libvorbisfile.dll.a + VORBIS_DLL - Only if building with sound on Windows; paths to vorbis DLLs + VORBIS_INCLUDE_DIR - Only if building with sound; directory that contains a directory vorbis with vorbisenc.h inside + VORBIS_LIBRARY - Only if building with sound; path to libvorbis.a/libvorbis.so/libvorbis.dll.a + ZLIB_DLL - Only on Windows; path to zlib1.dll + ZLIB_INCLUDE_DIR - Directory that contains zlib.h + ZLIB_LIBRARY - Path to libz.a/libz.so/zlib.lib + ZSTD_DLL - Only on Windows; path to libzstd.dll + ZSTD_INCLUDE_DIR - Directory that contains zstd.h + ZSTD_LIBRARY - Path to libzstd.a/libzstd.so/ztd.lib + +### Compiling on Windows using MSVC + +### Requirements + +- [Visual Studio 2015 or newer](https://visualstudio.microsoft.com) +- [CMake](https://cmake.org/download/) +- [vcpkg](https://github.com/Microsoft/vcpkg) +- [Git](https://git-scm.com/downloads) + +### Compiling and installing the dependencies + +It is highly recommended to use vcpkg as package manager. + +After you successfully built vcpkg you can easily install the required libraries: +```powershell +vcpkg install zlib zstd curl[winssl] openal-soft libvorbis libogg libjpeg-turbo sqlite3 freetype luajit gmp jsoncpp opengl-registry --triplet x64-windows +``` + +- **Don't forget about IrrlichtMt.** The easiest way is to clone it to `lib/irrlichtmt` as described in the Linux section. +- `curl` is optional, but required to read the serverlist, `curl[winssl]` is required to use the content store. +- `openal-soft`, `libvorbis` and `libogg` are optional, but required to use sound. +- `luajit` is optional, it replaces the integrated Lua interpreter with a faster just-in-time interpreter. +- `gmp` and `jsoncpp` are optional, otherwise the bundled versions will be compiled + +There are other optional libraries, but they are not tested if they can build and link correctly. + +Use `--triplet` to specify the target triplet, e.g. `x64-windows` or `x86-windows`. + +### Compile Minetest + +#### a) Using the vcpkg toolchain and CMake GUI +1. Start up the CMake GUI +2. Select **Browse Source...** and select DIR/minetest +3. Select **Browse Build...** and select DIR/minetest-build +4. Select **Configure** +5. Choose the right visual Studio version and target platform. It has to match the version of the installed dependencies +6. Choose **Specify toolchain file for cross-compiling** +7. Click **Next** +8. Select the vcpkg toolchain file e.g. `D:/vcpkg/scripts/buildsystems/vcpkg.cmake` +9. Click Finish +10. Wait until cmake have generated the cash file +11. If there are any errors, solve them and hit **Configure** +12. Click **Generate** +13. Click **Open Project** +14. Compile Minetest inside Visual studio. + +#### b) Using the vcpkg toolchain and the commandline + +Run the following script in PowerShell: + +```powershell +cmake . -G"Visual Studio 15 2017 Win64" -DCMAKE_TOOLCHAIN_FILE=D:/vcpkg/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_GETTEXT=OFF -DENABLE_CURSES=OFF +cmake --build . --config Release +``` +Make sure that the right compiler is selected and the path to the vcpkg toolchain is correct. + +### Windows Installer using WiX Toolset + +Requirements: +* [Visual Studio 2017](https://visualstudio.microsoft.com/) +* [WiX Toolset](https://wixtoolset.org/) + +In the Visual Studio 2017 Installer select **Optional Features -> WiX Toolset**. + +Build the binaries as described above, but make sure you unselect `RUN_IN_PLACE`. + +Open the generated project file with Visual Studio. Right-click **Package** and choose **Generate**. +It may take some minutes to generate the installer. + +### Compiling on MacOS + +#### Requirements +- [Homebrew](https://brew.sh/) +- [Git](https://git-scm.com/downloads) + +Install dependencies with homebrew: + +``` +brew install cmake freetype gettext gmp hiredis jpeg jsoncpp leveldb libogg libpng libvorbis luajit zstd +``` + +#### Download + +Download source (this is the URL to the latest of source repository, which might not work at all times) using Git: + +```bash +git clone --depth 1 https://github.com/minetest/minetest.git +cd minetest +``` + +Download minetest_game (otherwise only the "Development Test" game is available) using Git: + +``` +git clone --depth 1 https://github.com/minetest/minetest_game.git games/minetest_game +``` + +Download Minetest's fork of Irrlicht: + +``` +git clone --depth 1 https://github.com/minetest/irrlicht.git lib/irrlichtmt +``` + +#### Build + +```bash +mkdir build +cd build + +cmake .. \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.14 \ + -DCMAKE_FIND_FRAMEWORK=LAST \ + -DCMAKE_INSTALL_PREFIX=../build/macos/ \ + -DRUN_IN_PLACE=FALSE -DENABLE_GETTEXT=TRUE + +make -j$(sysctl -n hw.logicalcpu) +make install +``` + +#### Run + +``` +open ./build/macos/minetest.app +``` + +Docker +------ +We provide Minetest server Docker images using the GitLab mirror registry. + +Images are built on each commit and available using the following tag scheme: + +* `registry.gitlab.com/minetest/minetest/server:latest` (latest build) +* `registry.gitlab.com/minetest/minetest/server:` (current branch or current tag) +* `registry.gitlab.com/minetest/minetest/server:` (current commit id) + +If you want to test it on a Docker server you can easily run: + + sudo docker run registry.gitlab.com/minetest/minetest/server: + +If you want to use it in a production environment you should use volumes bound to the Docker host +to persist data and modify the configuration: + + sudo docker create -v /home/minetest/data/:/var/lib/minetest/ -v /home/minetest/conf/:/etc/minetest/ registry.gitlab.com/minetest/minetest/server:master + +Data will be written to `/home/minetest/data` on the host, and configuration will be read from `/home/minetest/conf/minetest.conf`. + +**Note:** If you don't understand the previous commands please read the official Docker documentation before use. + +You can also host your Minetest server inside a Kubernetes cluster. See our example implementation in [`misc/kubernetes.yml`](misc/kubernetes.yml). + + +Version scheme +-------------- +We use `major.minor.patch` since 5.0.0-dev. Prior to that we used `0.major.minor`. + +- Major is incremented when the release contains breaking changes, all other +numbers are set to 0. +- Minor is incremented when the release contains new non-breaking features, +patch is set to 0. +- Patch is incremented when the release only contains bugfixes and very +minor/trivial features considered necessary. + +Since 5.0.0-dev and 0.4.17-dev, the dev notation refers to the next release, +i.e.: 5.0.0-dev is the development version leading to 5.0.0. +Prior to that we used `previous_version-dev`. diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..e0613f8 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,11 @@ +*.iml +.externalNativeBuild +.gradle +app/build +app/release +app/src/main/assets +build +local.properties +native/.* +native/build +native/deps diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..ce895ed --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,116 @@ +apply plugin: 'com.android.application' +android { + compileSdkVersion 30 + buildToolsVersion '30.0.3' + ndkVersion "$ndk_version" + defaultConfig { + applicationId 'net.minetest.minetest' + minSdkVersion 16 + targetSdkVersion 30 + versionName "${versionMajor}.${versionMinor}.${versionPatch}" + versionCode project.versionCode + } + + // load properties + Properties props = new Properties() + def propfile = file('../local.properties') + if (propfile.exists()) + props.load(new FileInputStream(propfile)) + + if (props.getProperty('keystore') != null) { + signingConfigs { + release { + storeFile file(props['keystore']) + storePassword props['keystore.password'] + keyAlias props['key'] + keyPassword props['key.password'] + } + } + + buildTypes { + release { + minifyEnabled true + signingConfig signingConfigs.release + } + } + } + + // for multiple APKs + splits { + abi { + enable true + reset() + include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +task prepareAssets() { + def assetsFolder = "build/assets" + def projRoot = "../.." + def gameToCopy = "minetest_game" + + copy { + from "${projRoot}/minetest.conf.example", "${projRoot}/README.md" into assetsFolder + } + copy { + from "${projRoot}/doc/lgpl-2.1.txt" into "${assetsFolder}" + } + copy { + from "${projRoot}/builtin" into "${assetsFolder}/builtin" + } + copy { + from "${projRoot}/client/shaders" into "${assetsFolder}/client/shaders" + } + copy { + from "../native/deps/armeabi-v7a/Irrlicht/Shaders" into "${assetsFolder}/client/shaders/Irrlicht" + } + copy { + from "${projRoot}/fonts" include "*.ttf" into "${assetsFolder}/fonts" + } + copy { + from "${projRoot}/games/${gameToCopy}" into "${assetsFolder}/games/${gameToCopy}" + } + fileTree("${projRoot}/po").include("**/*.po").forEach { poFile -> + def moPath = "${assetsFolder}/locale/${poFile.parentFile.name}/LC_MESSAGES/" + file(moPath).mkdirs() + exec { + commandLine 'msgfmt', '-o', "${moPath}/minetest.mo", poFile + } + } + copy { + from "${projRoot}/textures" into "${assetsFolder}/textures" + } + + file("${assetsFolder}/.nomedia").text = ""; + + task zipAssets(type: Zip) { + archiveName "Minetest.zip" + from "${assetsFolder}" + destinationDir file("src/main/assets") + } +} + +preBuild.dependsOn zipAssets + +// Map for the version code that gives each ABI a value. +import com.android.build.OutputFile + +def abiCodes = ['armeabi-v7a': 0, 'arm64-v8a': 1] +android.applicationVariants.all { variant -> + variant.outputs.each { + output -> + def abiName = output.getFilter(OutputFile.ABI) + output.versionCodeOverride = abiCodes.get(abiName, 0) + variant.versionCode + } +} + +dependencies { + implementation project(':native') + implementation 'androidx.appcompat:appcompat:1.3.1' +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..11c8686 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/net/minetest/minetest/CustomEditText.java b/android/app/src/main/java/net/minetest/minetest/CustomEditText.java new file mode 100644 index 0000000..8d0a503 --- /dev/null +++ b/android/app/src/main/java/net/minetest/minetest/CustomEditText.java @@ -0,0 +1,45 @@ +/* +Minetest +Copyright (C) 2014-2020 MoNTE48, Maksim Gamarnik +Copyright (C) 2014-2020 ubulem, Bektur Mambetov + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +package net.minetest.minetest; + +import android.content.Context; +import android.view.KeyEvent; +import android.view.inputmethod.InputMethodManager; + +import androidx.appcompat.widget.AppCompatEditText; + +import java.util.Objects; + +public class CustomEditText extends AppCompatEditText { + public CustomEditText(Context context) { + super(context); + } + + @Override + public boolean onKeyPreIme(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + InputMethodManager mgr = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + Objects.requireNonNull(mgr).hideSoftInputFromWindow(this.getWindowToken(), 0); + } + return false; + } +} diff --git a/android/app/src/main/java/net/minetest/minetest/GameActivity.java b/android/app/src/main/java/net/minetest/minetest/GameActivity.java new file mode 100644 index 0000000..f5e9fd6 --- /dev/null +++ b/android/app/src/main/java/net/minetest/minetest/GameActivity.java @@ -0,0 +1,207 @@ +/* +Minetest +Copyright (C) 2014-2020 MoNTE48, Maksim Gamarnik +Copyright (C) 2014-2020 ubulem, Bektur Mambetov + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +package net.minetest.minetest; + +import android.app.NativeActivity; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.text.InputType; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; + +import androidx.annotation.Keep; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.FileProvider; + +import java.io.File; +import java.util.Objects; + +// Native code finds these methods by name (see porting_android.cpp). +// This annotation prevents the minifier/Proguard from mangling them. +@Keep +public class GameActivity extends NativeActivity { + static { + System.loadLibrary("c++_shared"); + System.loadLibrary("Minetest"); + } + + private int messageReturnCode = -1; + private String messageReturnValue = ""; + + public static native void putMessageBoxResult(String text); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void makeFullScreen() { + if (Build.VERSION.SDK_INT >= 19) + this.getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) + makeFullScreen(); + } + + @Override + protected void onResume() { + super.onResume(); + makeFullScreen(); + } + + @Override + public void onBackPressed() { + // Ignore the back press so Minetest can handle it + } + + public void showDialog(String acceptButton, String hint, String current, int editType) { + runOnUiThread(() -> showDialogUI(hint, current, editType)); + } + + private void showDialogUI(String hint, String current, int editType) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + LinearLayout container = new LinearLayout(this); + container.setOrientation(LinearLayout.VERTICAL); + builder.setView(container); + AlertDialog alertDialog = builder.create(); + EditText editText; + // For multi-line, do not close the dialog after pressing back button + if (editType == 1) { + editText = new EditText(this); + } else { + editText = new CustomEditText(this); + } + container.addView(editText); + editText.setMaxLines(8); + editText.requestFocus(); + editText.setHint(hint); + editText.setText(current); + final InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + Objects.requireNonNull(imm).toggleSoftInput(InputMethodManager.SHOW_FORCED, + InputMethodManager.HIDE_IMPLICIT_ONLY); + if (editType == 1) + editText.setInputType(InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_FLAG_MULTI_LINE); + else if (editType == 3) + editText.setInputType(InputType.TYPE_CLASS_TEXT | + InputType.TYPE_TEXT_VARIATION_PASSWORD); + else + editText.setInputType(InputType.TYPE_CLASS_TEXT); + editText.setSelection(editText.getText().length()); + editText.setOnKeyListener((view, keyCode, event) -> { + // For multi-line, do not submit the text after pressing Enter key + if (keyCode == KeyEvent.KEYCODE_ENTER && editType != 1) { + imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); + messageReturnCode = 0; + messageReturnValue = editText.getText().toString(); + alertDialog.dismiss(); + return true; + } + return false; + }); + // For multi-line, add Done button since Enter key does not submit text + if (editType == 1) { + Button doneButton = new Button(this); + container.addView(doneButton); + doneButton.setText(R.string.ime_dialog_done); + doneButton.setOnClickListener((view -> { + imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); + messageReturnCode = 0; + messageReturnValue = editText.getText().toString(); + alertDialog.dismiss(); + })); + } + alertDialog.show(); + alertDialog.setOnCancelListener(dialog -> { + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + messageReturnValue = current; + messageReturnCode = -1; + }); + } + + public int getDialogState() { + return messageReturnCode; + } + + public String getDialogValue() { + messageReturnCode = -1; + return messageReturnValue; + } + + public float getDensity() { + return getResources().getDisplayMetrics().density; + } + + public int getDisplayHeight() { + return getResources().getDisplayMetrics().heightPixels; + } + + public int getDisplayWidth() { + return getResources().getDisplayMetrics().widthPixels; + } + + public void openURI(String uri) { + Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri)); + startActivity(browserIntent); + } + + public String getUserDataPath() { + return Utils.getUserDataDirectory(this).getAbsolutePath(); + } + + public String getCachePath() { + return Utils.getCacheDirectory(this).getAbsolutePath(); + } + + public void shareFile(String path) { + File file = new File(path); + if (!file.exists()) { + Log.e("GameActivity", "File " + file.getAbsolutePath() + " doesn't exist"); + return; + } + + Uri fileUri = FileProvider.getUriForFile(this, "net.minetest.minetest.fileprovider", file); + + Intent intent = new Intent(Intent.ACTION_SEND, fileUri); + intent.setDataAndType(fileUri, getContentResolver().getType(fileUri)); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.putExtra(Intent.EXTRA_STREAM, fileUri); + + Intent shareIntent = Intent.createChooser(intent, null); + startActivity(shareIntent); + } +} diff --git a/android/app/src/main/java/net/minetest/minetest/MainActivity.java b/android/app/src/main/java/net/minetest/minetest/MainActivity.java new file mode 100644 index 0000000..b6567b4 --- /dev/null +++ b/android/app/src/main/java/net/minetest/minetest/MainActivity.java @@ -0,0 +1,185 @@ +/* +Minetest +Copyright (C) 2014-2020 MoNTE48, Maksim Gamarnik +Copyright (C) 2014-2020 ubulem, Bektur Mambetov + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +package net.minetest.minetest; + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static net.minetest.minetest.UnzipService.*; + +public class MainActivity extends AppCompatActivity { + private final static int versionCode = BuildConfig.VERSION_CODE; + private final static int PERMISSIONS = 1; + private static final String[] REQUIRED_SDK_PERMISSIONS = + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; + private static final String SETTINGS = "MinetestSettings"; + private static final String TAG_VERSION_CODE = "versionCode"; + + private ProgressBar mProgressBar; + private TextView mTextView; + private SharedPreferences sharedPreferences; + + private final BroadcastReceiver myReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int progress = 0; + @StringRes int message = 0; + if (intent != null) { + progress = intent.getIntExtra(ACTION_PROGRESS, 0); + message = intent.getIntExtra(ACTION_PROGRESS_MESSAGE, 0); + } + + if (progress == FAILURE) { + Toast.makeText(MainActivity.this, intent.getStringExtra(ACTION_FAILURE), Toast.LENGTH_LONG).show(); + finish(); + } else if (progress == SUCCESS) { + startNative(); + } else { + if (mProgressBar != null) { + mProgressBar.setVisibility(View.VISIBLE); + if (progress == INDETERMINATE) { + mProgressBar.setIndeterminate(true); + } else { + mProgressBar.setIndeterminate(false); + mProgressBar.setProgress(progress); + } + } + mTextView.setVisibility(View.VISIBLE); + if (message != 0) + mTextView.setText(message); + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + IntentFilter filter = new IntentFilter(ACTION_UPDATE); + registerReceiver(myReceiver, filter); + mProgressBar = findViewById(R.id.progressBar); + mTextView = findViewById(R.id.textView); + sharedPreferences = getSharedPreferences(SETTINGS, Context.MODE_PRIVATE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + Build.VERSION.SDK_INT < Build.VERSION_CODES.R) + checkPermission(); + else + checkAppVersion(); + } + + private void checkPermission() { + final List missingPermissions = new ArrayList<>(); + for (final String permission : REQUIRED_SDK_PERMISSIONS) { + final int result = ContextCompat.checkSelfPermission(this, permission); + if (result != PackageManager.PERMISSION_GRANTED) + missingPermissions.add(permission); + } + if (!missingPermissions.isEmpty()) { + final String[] permissions = missingPermissions + .toArray(new String[0]); + ActivityCompat.requestPermissions(this, permissions, PERMISSIONS); + } else { + final int[] grantResults = new int[REQUIRED_SDK_PERMISSIONS.length]; + Arrays.fill(grantResults, PackageManager.PERMISSION_GRANTED); + onRequestPermissionsResult(PERMISSIONS, REQUIRED_SDK_PERMISSIONS, grantResults); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, + @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == PERMISSIONS) { + for (int grantResult : grantResults) { + if (grantResult != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, R.string.not_granted, Toast.LENGTH_LONG).show(); + finish(); + return; + } + } + checkAppVersion(); + } + } + + private void checkAppVersion() { + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + Toast.makeText(this, R.string.no_external_storage, Toast.LENGTH_LONG).show(); + finish(); + return; + } + + if (UnzipService.getIsRunning()) { + mProgressBar.setVisibility(View.VISIBLE); + mProgressBar.setIndeterminate(true); + mTextView.setVisibility(View.VISIBLE); + } else if (sharedPreferences.getInt(TAG_VERSION_CODE, 0) == versionCode && + Utils.isInstallValid(this)) { + startNative(); + } else { + mProgressBar.setVisibility(View.VISIBLE); + mProgressBar.setIndeterminate(true); + mTextView.setVisibility(View.VISIBLE); + + Intent intent = new Intent(this, UnzipService.class); + startService(intent); + } + } + + private void startNative() { + sharedPreferences.edit().putInt(TAG_VERSION_CODE, versionCode).apply(); + Intent intent = new Intent(this, GameActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } + + @Override + public void onBackPressed() { + // Prevent abrupt interruption when copy game files from assets + } + + @Override + protected void onDestroy() { + super.onDestroy(); + unregisterReceiver(myReceiver); + } +} diff --git a/android/app/src/main/java/net/minetest/minetest/UnzipService.java b/android/app/src/main/java/net/minetest/minetest/UnzipService.java new file mode 100644 index 0000000..a61a491 --- /dev/null +++ b/android/app/src/main/java/net/minetest/minetest/UnzipService.java @@ -0,0 +1,259 @@ +/* +Minetest +Copyright (C) 2014-2020 MoNTE48, Maksim Gamarnik +Copyright (C) 2014-2020 ubulem, Bektur Mambetov + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +package net.minetest.minetest; + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Environment; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.StringRes; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +public class UnzipService extends IntentService { + public static final String ACTION_UPDATE = "net.minetest.minetest.UPDATE"; + public static final String ACTION_PROGRESS = "net.minetest.minetest.PROGRESS"; + public static final String ACTION_PROGRESS_MESSAGE = "net.minetest.minetest.PROGRESS_MESSAGE"; + public static final String ACTION_FAILURE = "net.minetest.minetest.FAILURE"; + public static final int SUCCESS = -1; + public static final int FAILURE = -2; + public static final int INDETERMINATE = -3; + private final int id = 1; + private NotificationManager mNotifyManager; + private boolean isSuccess = true; + private String failureMessage; + + private static boolean isRunning = false; + public static synchronized boolean getIsRunning() { + return isRunning; + } + private static synchronized void setIsRunning(boolean v) { + isRunning = v; + } + + public UnzipService() { + super("net.minetest.minetest.UnzipService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + Notification.Builder notificationBuilder = createNotification(); + final File zipFile = new File(getCacheDir(), "Minetest.zip"); + try { + setIsRunning(true); + File userDataDirectory = Utils.getUserDataDirectory(this); + if (userDataDirectory == null) { + throw new IOException("Unable to find user data directory"); + } + + try (InputStream in = this.getAssets().open(zipFile.getName())) { + try (OutputStream out = new FileOutputStream(zipFile)) { + int readLen; + byte[] readBuffer = new byte[16384]; + while ((readLen = in.read(readBuffer)) != -1) { + out.write(readBuffer, 0, readLen); + } + } + } + + migrate(notificationBuilder, userDataDirectory); + unzip(notificationBuilder, zipFile, userDataDirectory); + } catch (IOException e) { + isSuccess = false; + failureMessage = e.getLocalizedMessage(); + } finally { + setIsRunning(false); + zipFile.delete(); + } + } + + private Notification.Builder createNotification() { + String name = "net.minetest.minetest"; + String channelId = "Minetest channel"; + String description = "notifications from Minetest"; + Notification.Builder builder; + if (mNotifyManager == null) + mNotifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int importance = NotificationManager.IMPORTANCE_LOW; + NotificationChannel mChannel = null; + if (mNotifyManager != null) + mChannel = mNotifyManager.getNotificationChannel(channelId); + if (mChannel == null) { + mChannel = new NotificationChannel(channelId, name, importance); + mChannel.setDescription(description); + // Configure the notification channel, NO SOUND + mChannel.setSound(null, null); + mChannel.enableLights(false); + mChannel.enableVibration(false); + mNotifyManager.createNotificationChannel(mChannel); + } + builder = new Notification.Builder(this, channelId); + } else { + builder = new Notification.Builder(this); + } + + Intent notificationIntent = new Intent(this, MainActivity.class); + notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_SINGLE_TOP); + PendingIntent intent = PendingIntent.getActivity(this, 0, + notificationIntent, 0); + + builder.setContentTitle(getString(R.string.notification_title)) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentText(getString(R.string.notification_description)) + .setContentIntent(intent) + .setOngoing(true) + .setProgress(0, 0, true); + + mNotifyManager.notify(id, builder.build()); + return builder; + } + + private void unzip(Notification.Builder notificationBuilder, File zipFile, File userDataDirectory) throws IOException { + int per = 0; + + int size; + try (ZipFile zipSize = new ZipFile(zipFile)) { + size = zipSize.size(); + } + + int readLen; + byte[] readBuffer = new byte[16384]; + try (FileInputStream fileInputStream = new FileInputStream(zipFile); + ZipInputStream zipInputStream = new ZipInputStream(fileInputStream)) { + ZipEntry ze; + while ((ze = zipInputStream.getNextEntry()) != null) { + if (ze.isDirectory()) { + ++per; + Utils.createDirs(userDataDirectory, ze.getName()); + continue; + } + publishProgress(notificationBuilder, R.string.loading, 100 * ++per / size); + try (OutputStream outputStream = new FileOutputStream( + new File(userDataDirectory, ze.getName()))) { + while ((readLen = zipInputStream.read(readBuffer)) != -1) { + outputStream.write(readBuffer, 0, readLen); + } + } + } + } + } + + void moveFileOrDir(@NonNull File src, @NonNull File dst) throws IOException { + try { + Process p = new ProcessBuilder("/system/bin/mv", + src.getAbsolutePath(), dst.getAbsolutePath()).start(); + int exitcode = p.waitFor(); + if (exitcode != 0) + throw new IOException("Move failed with exit code " + exitcode); + } catch (InterruptedException e) { + throw new IOException("Move operation interrupted"); + } + } + + boolean recursivelyDeleteDirectory(@NonNull File loc) { + try { + Process p = new ProcessBuilder("/system/bin/rm", "-rf", + loc.getAbsolutePath()).start(); + return p.waitFor() == 0; + } catch (IOException | InterruptedException e) { + return false; + } + } + + /** + * Migrates user data from deprecated external storage to app scoped storage + */ + private void migrate(Notification.Builder notificationBuilder, File newLocation) throws IOException { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return; + } + + File oldLocation = new File(Environment.getExternalStorageDirectory(), "Minetest"); + if (!oldLocation.isDirectory()) + return; + + publishProgress(notificationBuilder, R.string.migrating, 0); + newLocation.mkdir(); + + String[] dirs = new String[] { "worlds", "games", "mods", "textures", "client" }; + for (int i = 0; i < dirs.length; i++) { + publishProgress(notificationBuilder, R.string.migrating, 100 * i / dirs.length); + File dir = new File(oldLocation, dirs[i]), dir2 = new File(newLocation, dirs[i]); + if (dir.isDirectory() && !dir2.isDirectory()) { + moveFileOrDir(dir, dir2); + } + } + + for (String filename : new String[] { "minetest.conf" }) { + File file = new File(oldLocation, filename), file2 = new File(newLocation, filename); + if (file.isFile() && !file2.isFile()) { + moveFileOrDir(file, file2); + } + } + + recursivelyDeleteDirectory(oldLocation); + } + + private void publishProgress(@Nullable Notification.Builder notificationBuilder, @StringRes int message, int progress) { + Intent intentUpdate = new Intent(ACTION_UPDATE); + intentUpdate.putExtra(ACTION_PROGRESS, progress); + intentUpdate.putExtra(ACTION_PROGRESS_MESSAGE, message); + if (!isSuccess) + intentUpdate.putExtra(ACTION_FAILURE, failureMessage); + sendBroadcast(intentUpdate); + + if (notificationBuilder != null) { + notificationBuilder.setContentText(getString(message)); + if (progress == INDETERMINATE) { + notificationBuilder.setProgress(100, 50, true); + } else { + notificationBuilder.setProgress(100, progress, false); + } + mNotifyManager.notify(id, notificationBuilder.build()); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + mNotifyManager.cancel(id); + publishProgress(null, R.string.loading, isSuccess ? SUCCESS : FAILURE); + } +} diff --git a/android/app/src/main/java/net/minetest/minetest/Utils.java b/android/app/src/main/java/net/minetest/minetest/Utils.java new file mode 100644 index 0000000..b2553c8 --- /dev/null +++ b/android/app/src/main/java/net/minetest/minetest/Utils.java @@ -0,0 +1,39 @@ +package net.minetest.minetest; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.io.File; + +public class Utils { + public static @NonNull File createDirs(File root, String dir) { + File f = new File(root, dir); + if (!f.isDirectory()) + f.mkdirs(); + + return f; + } + + public static @Nullable File getUserDataDirectory(Context context) { + File extDir = context.getExternalFilesDir(null); + if (extDir == null) { + return null; + } + + return createDirs(extDir, "Minetest"); + } + + public static @Nullable File getCacheDirectory(Context context) { + return context.getCacheDir(); + } + + public static boolean isInstallValid(Context context) { + File userDataDirectory = getUserDataDirectory(context); + return userDataDirectory != null && userDataDirectory.isDirectory() && + new File(userDataDirectory, "games").isDirectory() && + new File(userDataDirectory, "builtin").isDirectory() && + new File(userDataDirectory, "client").isDirectory() && + new File(userDataDirectory, "textures").isDirectory(); + } +} diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000000000000000000000000000000000000..43bd6089ec30a1ebd68f90296cb8d4633c0dde14 GIT binary patch literal 83 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnL3?x0byx0z;m;-!5Tzhu?;t_A+19G`NT^vIy c;*uqv067a77-dYInSd+?Pgg&ebxsLQ0GX;0bN~PV literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable/bg.xml b/android/app/src/main/res/drawable/bg.xml new file mode 100644 index 0000000..903335e --- /dev/null +++ b/android/app/src/main/res/drawable/bg.xml @@ -0,0 +1,4 @@ + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..93508c3 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/android/app/src/main/res/mipmap/ic_launcher.png b/android/app/src/main/res/mipmap/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..88a83782c6a6d1fe866a0eb8ef5355398dc2e1a8 GIT binary patch literal 5780 zcmV;F7HjE=P)9x zFZ;4D8?O<$G&eg7U9k9aniE6iFcsn72=6m;97oz&>7>QT>6y$>_521A4-~Q;9x!qo zmW;`u^|3Q#qs3G@jzwf2AxY;HIY7uPI*7*80g+D%@dGo5UMQE-dfFMY(&DIS0~jj$ z21M{fOfstg5kK7#WUI7@eAN;BD|DU+mBZ(Ls-97H_8wzNRJ7hql|w_T&jSc90~VTkHVg^XVoLZ;0sE=^&h-6CYF#cbM69HLZcx z!_Kt98dXAbMFh=3_&>sK(C(Gg(}~?|3HiE+Kmu}0w6=t->@W`KcGgL|$?2Jmh`CJ= zgmx?5wRrbFF7bL;=?K9y9D$Wj3Gt_$c`~nW-I=^H2Z&tgXkFqBfCD)B^X$wU2!MB~ zEy*Wgq5w}ZGMv(wnNDA_GJ9WPDV(GK`U0%#9s+Qa-Na0V({V&T=%e=@wxZVvy8t#4 zAT-Klj5yKdb5<+i+*~I}S?vi)%RGRc?Mw=BpOxXXkR|WqVHdzk0`MbfeZ@&GM_B33 z4>8GXr86Wnc|gUPeo%W^gNoBWAY0@P=*b|3_z79m#8GmZqg?>Y1$ZhT-enOt)#(9g zGokgaP<+&zBHj?{1Ktok(N)))K4WA$OYH(!Ab@|0)4xd~#g(p*v$JoT#2X@eTVD{A zxabPe!pZ18%r1Z#0+2qR`lct;TP@H+tQNf4%V0HnlL_Uruygc#=aDs5DOPd8BOGz7w<{kUdwfqweoXRB)GzYr? z79w*?AK({j?I%v*14&{xNENsl6W}R4HbvL)FTJ_8%Hbj!sRq~uFaw1LQC%?EjUry0 z?h31C1;F?FD+uz93DCOhHG`&FI)u!01h$&qbxp#qYQ{Yd@#Uv9nGdTq^WmmDLtV`n z_-;=Le7#)_P30Vd{4@k*yu-K32q&)L{f5n^>rL^xT!7AQM_WRC^#x7tPx(B8GS3$- zy`_RXd&;1BS}3T3y)@*D+(8-B8*=pA`n8P^!%gS31D<2 znd|V-RwK#adM7A6y3K%UKUrhx}G0jG7xHWpQnf~ zm3hO5+x+3dy*RVUH${N#c!%#ZrM<#Cm-sl>k)lNB>*P2Pw|EWP#5iz9=oBo*4(Ix3 zJAeX-ReS02i!3}J5Larm9&wU0ENFNR{_|~&MZ}vb0Fg1_v{gnwqlj=GXj9^OQkr|9 z%y|zx-H8|n;=(3ozB`nkFy1C^oYV`d6g?m~kFHz70}DZk(3su>CLQW))&5TUhCaZ{ z*Te&SeTvFH@XaS7HWOgoYtKSndQVN-D)0GF8RNK3Zi~@pP^{}TMTq1bweBz(?M_|}9ny+=!`qWQUxdFl@S@tgt>~|FlE*d_|<}^p(6qC zYrZ}g2(u=8Q}u%?$>GvoM6C}H6eI7Njl+ArjvJFyz-Q(Ti*wj9-e~KsZ zD~2rv&<)U`Q?xT-vC%^+ zVmlPzn~O2<`yK7q;Fz<0-HT9^MW^C}Qbs?SFLUW{GWl|ek1Ib}Yd2x9xIytT6Ny4k zM(MFZkhgUx%&c(gP=Jy*214PUAy9q3zfDD`I`0FC%RH$wLu`b`#lwse|Gzh7@ad^2 zIDJssp#befnS|L9D3)koe@I zDERs7SiSFg2|a0bl^$e}Hmz=u^WFQ(g{~lt^8lVwxO`cQHx4 zp#YmpoZ#pbE%|#Upaz(tr@jEK$lLA7FWt1)o? zNR%F-x&lNkaWvYFHhewC7iXglgpH-11d|28MNCeJH(cAE4|UTR=#kYt5xhKY&R=t} zKa?Cq-NSc=K0-dm)R)}|k};Ml9bkKvE38+ez?`A^yZNXNC|p0-=+_qP^o6R^10I|B zqQl;Lg<5n`f7tm(JVgj8z#U{E#sm-+I#HFn_~q7tOd-AV1i)oI#_`P=ps0F=x)EWK zC&-r?Cr=mEi+o^So+Bj$fy^E-rOq7+HV*0V>n$a|0u5nsjl#;(Y#ecpJFHq947c}| z!OdOe5D+$;z>i;PQ`7r*vOPmdpdYf}wQAP++R`YnfQoE#0`uUyt~!C!*nB zH}o5PmyT*J=&=OAxVNtY-d!1kDqJ66rn&{1d?bmN* z#4gq)x4Bse$rHRms7Aqi>1a54KpQ76d=Ldce4+3C-(3!YMKcCa#9KuG61UJOZ{a)< ziVx_~PDMd2*$2WW4FJ{V_Q}lOJj7(;)w_m3d(yJK-jxANRPyyuFK%^hgw@YbwCOq>;${zq91}@4DiR996(Hz`uV!P(l5BP;3BPJ`t%` zVBIwmAHFyj4ga~VGe5YinIBZ9`brysmFIS3eX% zeQh6#G-R^Q|oUDZ5f>?sg!K_#a%$W&g%TGK)ydjF4j)5S( z4hAwcqCMvy3iIX$w%N+QcP$KFpQD{2SEyWIcT*_bLtN-`b7yZkL`Mz+CXZ=50Z3d& zBw#YQS+2lH@c>qe4@m0=LCzKsxje?Kg$tHgG|IE(?|L}OqPS}Yx zkpJp|4v9BJ_M8?NBQ2u745} zWkZZK5XQ2(1Z*w<5VpG-+%8HuHR(3z^g3HJf-nto;}gUV0OT2~(->8!Z>Xdz)BLQ|L_X`gs$E!PVA4uZxRY z)KIL9q=@fy0Z8Crh=U?C%?+ikh{P%{C_G^LG4s;n-Viavjq>@ByuO6hVaxk+dnweG zXXyF7wCpvgdgJWlk*|ZYW zpGOeR@)v+IS7kc^CKkuQXIqQgeGM*ta1n%yA!1OHr5WV3cizA1oDcEdFHW}B=W*(L zb9XU1(N+$N>a$^lP)3k42zjZj>7|aMjEO?8lZ9BEb9qyW2gQP#^2Om$cLsWa=5*xtX&~+F@%dEwn9THH(%4% z(48&RT+&Rn_=Vc37EX1qw5D_KEXf1Dj?XjVDj|2lKARD52=eC|-dlQN2t~3aJOai~ z98VBW!fFBp1q)%*+zg_sBn2QLjR=5kixeF`H77P_LUzqCZDXv$6>@fXLEdgJ2&~lx zSVi?SaC}ob@iiZ=FM@;=IYq#T;A}{(f2-4o???cW19fpB%$Yk=k9?~NKz^gNgb?^( zZQdh61|vH)3{I^tXjcR*AolXG0xxQ(Q~7*;Us(ToIC`wh;nS_9P+pXda&yg%Kf_Ok zn5yMQ^Lbk$cm6)`lT{cM;P~2H>fQ~Bx0wJKejMH-a+%rYsTw$mgwrr}>)n z7H3*Gkny)Oqe-qTK5xxL==h-EltXHML4{s5TX;3wE28k z9?Ivpb_wx@o_~5{kx?FE65?UXD|KxW-)#gS(H8jyxoPMz*WB#0sQ8JiT@BT!80t#$ zO$oXk5#MD3NLd`~dpqag)zp?u}3E+_tp7ogma0U=0)3XaBj3Q*K^wCl)6F(>Ke^)O7F-rC}bhQjRJI}>r?|4(l|*ZU zc=A}N&??J-uii7>Q0&O_@e`3#kOypmu=TYZ5b|F9G@Sp;i&i-$ohI~PBLP0rd~@yY z76wWnJ}HhO-Vl+|Qeg7=)a!A(cE@Y&7i8qI5nBn6-p~xgQ8SNx+EUZyaBg$^EtB>H zsH~{~QLwff>dzVTb1?T=xz*nzAdyHMIBdV8x;t7Z!2-m$DF6n3GJ?ZGsmBr{AvOp; zdAqPv1yC~2kJNG>UtR*7D%`+?8=lr-#tI_Rm{U{r$M0b6C`PRO3U6Gla-slS(^nZmKPg%%TU_}LN&--PpgOK4BxLSETCmc@BU zk}W{R87|VW2obnO@YmJ|@i9Ril7o+=ES#!)^iW0|KP2o%B!i{XRTkR~opv9Y^G6F99Oh3{d}!?*>BaorNd{nb&blQK(>cQbK z_Ti?LZhn1flhWqoAAUU%%3=nS#6LvXfSZoA|Hj2)v4%1^jBm*EMCl?ASXRx28#{`v zPW-jEat!U}vYhW4NV6|gKd?t7p+Nn2vhAH4>P=)MI5T-%;%~K-5Ta&?JxJnz#&#o- zbkV2ZcIQR3y@hTcQXj0B^bFt(#ZvN#Pe_ayKob9FiokVA0@uyU&UE|hsZ_qJJXPR& zQ+F)->D##`6Mtbt7SyE=*R`9Ia*+qiPOTXW7eKfW!UYi~5oau`CkO~jX|sQXVh6Sx z982)mqzK%;M|Pv^Hchi9ab@zHhTu%4HDdvF~74 zG}>AVXDsNn#s$(=rewST=htV#sWoXZLFW?^5k5#2xs0lS~YwHC* S + + + Minetest + Loading… + Migrating save data from old install… (this may take a while) + Required permission wasn\'t granted, Minetest can\'t run without it + Loading Minetest + Less than 1 minute… + Done + External storage isn\'t available. If you use an SDCard, please reinsert it. Otherwise, try restarting your phone or contacting the Minetest developers + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..291a4ea --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/android/app/src/main/res/xml/filepaths.xml b/android/app/src/main/res/xml/filepaths.xml new file mode 100644 index 0000000..2fff069 --- /dev/null +++ b/android/app/src/main/res/xml/filepaths.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..8e7a8a9 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,37 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +project.ext.set("versionMajor", 5) // Version Major +project.ext.set("versionMinor", 6) // Version Minor +project.ext.set("versionPatch", 0) // Version Patch +project.ext.set("versionExtra", "") // Version Extra +project.ext.set("versionCode", 42) // Android Version Code +project.ext.set("developmentBuild", 0) // Whether it is a development build, or a release +// NOTE: +2 after each release! +// +1 for ARM and +1 for ARM64 APK's, because +// each APK must have a larger `versionCode` than the previous + +buildscript { + ext.ndk_version = '23.2.8568313' + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'de.undercouch:gradle-download-task:4.1.1' + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir + delete 'native/deps' +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..53b475c --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,11 @@ +<#if isLowMemory> +org.gradle.jvmargs=-Xmx4G -XX:MaxPermSize=2G -XX:+HeapDumpOnOutOfMemoryError +<#else> +org.gradle.jvmargs=-Xmx16G -XX:MaxPermSize=8G -XX:+HeapDumpOnOutOfMemoryError + +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.parallel.threads=8 +org.gradle.configureondemand=true +android.enableJetifier=true +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 zcmafaW0WS*vSoFbZJS-TZP!<}ZQEV8ZQHihW!tvx>6!c9%-lQoy;&DmfdT@8fB*sl68LLCKtKQ283+jS?^Q-bNq|NIAW8=eB==8_)^)r*{C^$z z{u;{v?IMYnO`JhmPq7|LA_@Iz75S9h~8`iX>QrjrmMeu{>hn4U;+$dor zz+`T8Q0f}p^Ao)LsYq74!W*)&dTnv}E8;7H*Zetclpo2zf_f>9>HT8;`O^F8;M%l@ z57Z8dk34kG-~Wg7n48qF2xwPp;SOUpd1}9Moir5$VSyf4gF)Mp-?`wO3;2x9gYj59oFwG>?Leva43@e(z{mjm0b*@OAYLC`O9q|s+FQLOE z!+*Y;%_0(6Sr<(cxE0c=lS&-FGBFGWd_R<5$vwHRJG=tB&Mi8@hq_U7@IMyVyKkOo6wgR(<% zQw1O!nnQl3T9QJ)Vh=(`cZM{nsEKChjbJhx@UQH+G>6p z;beBQ1L!3Zl>^&*?cSZjy$B3(1=Zyn~>@`!j%5v7IBRt6X`O)yDpVLS^9EqmHxBcisVG$TRwiip#ViN|4( zYn!Av841_Z@Ys=T7w#>RT&iXvNgDq3*d?$N(SznG^wR`x{%w<6^qj&|g})La;iD?`M=p>99p><39r9+e z`dNhQ&tol5)P#;x8{tT47i*blMHaDKqJs8!Pi*F{#)9%USFxTVMfMOy{mp2ZrLR40 z2a9?TJgFyqgx~|j0eA6SegKVk@|Pd|_6P$HvwTrLTK)Re`~%kg8o9`EAE1oAiY5Jgo=H}0*D?tSCn^=SIN~fvv453Ia(<1|s07aTVVtsRxY6+tT3589iQdi^ zC92D$ewm9O6FA*u*{Fe_=b`%q`pmFvAz@hfF@OC_${IPmD#QMpPNo0mE9U=Ch;k0L zZteokPG-h7PUeRCPPYG%H!WswC?cp7M|w42pbtwj!m_&4%hB6MdLQe&}@5-h~! zkOt;w0BbDc0H!RBw;1UeVckHpJ@^|j%FBZlC} zsm?nFOT$`F_i#1_gh4|n$rDe>0md6HvA=B%hlX*3Z%y@a&W>Rq`Fe(8smIgxTGb#8 zZ`->%h!?QCk>v*~{!qp=w?a*};Y**1uH`)OX`Gi+L%-d6{rV?@}MU#qfCU(!hLz;kWH=0A%W7E^pA zD;A%Jg5SsRe!O*0TyYkAHe&O9z*Ij-YA$%-rR?sc`xz_v{>x%xY39!8g#!Z0#03H( z{O=drKfb0cbx1F*5%q81xvTDy#rfUGw(fesh1!xiS2XT;7_wBi(Rh4i(!rR^9=C+- z+**b9;icxfq@<7}Y!PW-0rTW+A^$o*#ZKenSkxLB$Qi$%gJSL>x!jc86`GmGGhai9 zOHq~hxh}KqQHJeN$2U{M>qd*t8_e&lyCs69{bm1?KGTYoj=c0`rTg>pS6G&J4&)xp zLEGIHSTEjC0-s-@+e6o&w=h1sEWWvJUvezID1&exb$)ahF9`(6`?3KLyVL$|c)CjS zx(bsy87~n8TQNOKle(BM^>1I!2-CZ^{x6zdA}qeDBIdrfd-(n@Vjl^9zO1(%2pP9@ zKBc~ozr$+4ZfjmzEIzoth(k?pbI87=d5OfjVZ`Bn)J|urr8yJq`ol^>_VAl^P)>2r)s+*3z5d<3rP+-fniCkjmk=2hTYRa@t zCQcSxF&w%mHmA?!vaXnj7ZA$)te}ds+n8$2lH{NeD4mwk$>xZCBFhRy$8PE>q$wS`}8pI%45Y;Mg;HH+}Dp=PL)m77nKF68FggQ-l3iXlVZuM2BDrR8AQbK;bn1%jzahl0; zqz0(mNe;f~h8(fPzPKKf2qRsG8`+Ca)>|<&lw>KEqM&Lpnvig>69%YQpK6fx=8YFj zHKrfzy>(7h2OhUVasdwKY`praH?>qU0326-kiSyOU_Qh>ytIs^htlBA62xU6xg?*l z)&REdn*f9U3?u4$j-@ndD#D3l!viAUtw}i5*Vgd0Y6`^hHF5R=No7j8G-*$NWl%?t z`7Nilf_Yre@Oe}QT3z+jOUVgYtT_Ym3PS5(D>kDLLas8~F+5kW%~ZYppSrf1C$gL* zCVy}fWpZ3s%2rPL-E63^tA|8OdqKsZ4TH5fny47ENs1#^C`_NLg~H^uf3&bAj#fGV zDe&#Ot%_Vhj$}yBrC3J1Xqj>Y%&k{B?lhxKrtYy;^E9DkyNHk5#6`4cuP&V7S8ce9 zTUF5PQIRO7TT4P2a*4;M&hk;Q7&{(83hJe5BSm=9qt~;U)NTf=4uKUcnxC`;iPJeI zW#~w?HIOM+0j3ptB0{UU{^6_#B*Q2gs;1x^YFey(%DJHNWz@e_NEL?$fv?CDxG`jk zH|52WFdVsZR;n!Up;K;4E$|w4h>ZIN+@Z}EwFXI{w_`?5x+SJFY_e4J@|f8U08%dd z#Qsa9JLdO$jv)?4F@&z_^{Q($tG`?|9bzt8ZfH9P`epY`soPYqi1`oC3x&|@m{hc6 zs0R!t$g>sR@#SPfNV6Pf`a^E?q3QIaY30IO%yKjx#Njj@gro1YH2Q(0+7D7mM~c>C zk&_?9Ye>B%*MA+77$Pa!?G~5tm`=p{NaZsUsOgm6Yzclr_P^2)r(7r%n(0?4B#$e7 z!fP;+l)$)0kPbMk#WOjm07+e?{E)(v)2|Ijo{o1+Z8#8ET#=kcT*OwM#K68fSNo%< zvZFdHrOrr;>`zq!_welWh!X}=oN5+V01WJn7=;z5uo6l_$7wSNkXuh=8Y>`TjDbO< z!yF}c42&QWYXl}XaRr0uL?BNPXlGw=QpDUMo`v8pXzzG(=!G;t+mfCsg8 zJb9v&a)E!zg8|%9#U?SJqW!|oBHMsOu}U2Uwq8}RnWeUBJ>FtHKAhP~;&T4mn(9pB zu9jPnnnH0`8ywm-4OWV91y1GY$!qiQCOB04DzfDDFlNy}S{$Vg9o^AY!XHMueN<{y zYPo$cJZ6f7``tmlR5h8WUGm;G*i}ff!h`}L#ypFyV7iuca!J+C-4m@7*Pmj9>m+jh zlpWbud)8j9zvQ`8-oQF#u=4!uK4kMFh>qS_pZciyq3NC(dQ{577lr-!+HD*QO_zB9 z_Rv<#qB{AAEF8Gbr7xQly%nMA%oR`a-i7nJw95F3iH&IX5hhy3CCV5y>mK4)&5aC*12 zI`{(g%MHq<(ocY5+@OK-Qn-$%!Nl%AGCgHl>e8ogTgepIKOf3)WoaOkuRJQt%MN8W z=N-kW+FLw=1^}yN@*-_c>;0N{-B!aXy#O}`%_~Nk?{e|O=JmU8@+92Q-Y6h)>@omP=9i~ zi`krLQK^!=@2BH?-R83DyFkejZkhHJqV%^} zUa&K22zwz7b*@CQV6BQ9X*RB177VCVa{Z!Lf?*c~PwS~V3K{id1TB^WZh=aMqiws5)qWylK#^SG9!tqg3-)p_o(ABJsC!0;0v36;0tC= z!zMQ_@se(*`KkTxJ~$nIx$7ez&_2EI+{4=uI~dwKD$deb5?mwLJ~ema_0Z z6A8Q$1~=tY&l5_EBZ?nAvn$3hIExWo_ZH2R)tYPjxTH5mAw#3n-*sOMVjpUrdnj1DBm4G!J+Ke}a|oQN9f?!p-TcYej+(6FNh_A? zJ3C%AOjc<8%9SPJ)U(md`W5_pzYpLEMwK<_jgeg-VXSX1Nk1oX-{yHz z-;CW!^2ds%PH{L{#12WonyeK5A=`O@s0Uc%s!@22etgSZW!K<%0(FHC+5(BxsXW@e zAvMWiO~XSkmcz%-@s{|F76uFaBJ8L5H>nq6QM-8FsX08ug_=E)r#DC>d_!6Nr+rXe zzUt30Du_d0oSfX~u>qOVR*BmrPBwL@WhF^5+dHjWRB;kB$`m8|46efLBXLkiF|*W= zg|Hd(W}ZnlJLotYZCYKoL7YsQdLXZ!F`rLqLf8n$OZOyAzK`uKcbC-n0qoH!5-rh&k-`VADETKHxrhK<5C zhF0BB4azs%j~_q_HA#fYPO0r;YTlaa-eb)Le+!IeP>4S{b8&STp|Y0if*`-A&DQ$^ z-%=i73HvEMf_V6zSEF?G>G-Eqn+|k`0=q?(^|ZcqWsuLlMF2!E*8dDAx%)}y=lyMa z$Nn0_f8YN8g<4D>8IL3)GPf#dJYU@|NZqIX$;Lco?Qj=?W6J;D@pa`T=Yh z-ybpFyFr*3^gRt!9NnbSJWs2R-S?Y4+s~J8vfrPd_&_*)HBQ{&rW(2X>P-_CZU8Y9 z-32><7|wL*K+3{ZXE5}nn~t@NNT#Bc0F6kKI4pVwLrpU@C#T-&f{Vm}0h1N3#89@d zgcx3QyS;Pb?V*XAq;3(W&rjLBazm69XX;%^n6r}0!CR2zTU1!x#TypCr`yrII%wk8 z+g)fyQ!&xIX(*>?T}HYL^>wGC2E}euj{DD_RYKK@w=yF+44367X17)GP8DCmBK!xS zE{WRfQ(WB-v>DAr!{F2-cQKHIjIUnLk^D}7XcTI#HyjSiEX)BO^GBI9NjxojYfQza zWsX@GkLc7EqtP8(UM^cq5zP~{?j~*2T^Bb={@PV)DTkrP<9&hxDwN2@hEq~8(ZiF! z3FuQH_iHyQ_s-#EmAC5~K$j_$cw{+!T>dm#8`t%CYA+->rWp09jvXY`AJQ-l%C{SJ z1c~@<5*7$`1%b}n7ivSo(1(j8k+*Gek(m^rQ!+LPvb=xA@co<|(XDK+(tb46xJ4) zcw7w<0p3=Idb_FjQ@ttoyDmF?cT4JRGrX5xl&|ViA@Lg!vRR}p#$A?0=Qe+1)Mizl zn;!zhm`B&9t0GA67GF09t_ceE(bGdJ0mbXYrUoV2iuc3c69e;!%)xNOGG*?x*@5k( zh)snvm0s&gRq^{yyeE)>hk~w8)nTN`8HJRtY0~1f`f9ue%RV4~V(K*B;jFfJY4dBb z*BGFK`9M-tpWzayiD>p_`U(29f$R|V-qEB;+_4T939BPb=XRw~8n2cGiRi`o$2qm~ zN&5N7JU{L*QGM@lO8VI)fUA0D7bPrhV(GjJ$+@=dcE5vAVyCy6r&R#4D=GyoEVOnu z8``8q`PN-pEy>xiA_@+EN?EJpY<#}BhrsUJC0afQFx7-pBeLXR9Mr+#w@!wSNR7vxHy@r`!9MFecB4O zh9jye3iSzL0@t3)OZ=OxFjjyK#KSF|zz@K}-+HaY6gW+O{T6%Zky@gD$6SW)Jq;V0 zt&LAG*YFO^+=ULohZZW*=3>7YgND-!$2}2)Mt~c>JO3j6QiPC-*ayH2xBF)2m7+}# z`@m#q{J9r~Dr^eBgrF(l^#sOjlVNFgDs5NR*Xp;V*wr~HqBx7?qBUZ8w)%vIbhhe) zt4(#1S~c$Cq7b_A%wpuah1Qn(X9#obljoY)VUoK%OiQZ#Fa|@ZvGD0_oxR=vz{>U* znC(W7HaUDTc5F!T77GswL-jj7e0#83DH2+lS-T@_^SaWfROz9btt*5zDGck${}*njAwf}3hLqKGLTeV&5(8FC+IP>s;p{L@a~RyCu)MIa zs~vA?_JQ1^2Xc&^cjDq02tT_Z0gkElR0Aa$v@VHi+5*)1(@&}gEXxP5Xon?lxE@is z9sxd|h#w2&P5uHJxWgmtVZJv5w>cl2ALzri;r57qg){6`urTu(2}EI?D?##g=!Sbh z*L*>c9xN1a3CH$u7C~u_!g81`W|xp=54oZl9CM)&V9~ATCC-Q!yfKD@vp#2EKh0(S zgt~aJ^oq-TM0IBol!w1S2j7tJ8H7;SR7yn4-H}iz&U^*zW95HrHiT!H&E|rSlnCYr z7Y1|V7xebn=TFbkH;>WIH6H>8;0?HS#b6lCke9rSsH%3AM1#2U-^*NVhXEIDSFtE^ z=jOo1>j!c__Bub(R*dHyGa)@3h?!ls1&M)d2{?W5#1|M@6|ENYYa`X=2EA_oJUw=I zjQ)K6;C!@>^i7vdf`pBOjH>Ts$97}B=lkb07<&;&?f#cy3I0p5{1=?O*#8m$C_5TE zh}&8lOWWF7I@|pRC$G2;Sm#IJfhKW@^jk=jfM1MdJP(v2fIrYTc{;e5;5gsp`}X8-!{9{S1{h+)<@?+D13s^B zq9(1Pu(Dfl#&z|~qJGuGSWDT&u{sq|huEsbJhiqMUae}K*g+R(vG7P$p6g}w*eYWn zQ7luPl1@{vX?PMK%-IBt+N7TMn~GB z!Ldy^(2Mp{fw_0;<$dgHAv1gZgyJAx%}dA?jR=NPW1K`FkoY zNDgag#YWI6-a2#&_E9NMIE~gQ+*)i<>0c)dSRUMHpg!+AL;a;^u|M1jp#0b<+#14z z+#LuQ1jCyV_GNj#lHWG3e9P@H34~n0VgP#(SBX=v|RSuOiY>L87 z#KA{JDDj2EOBX^{`a;xQxHtY1?q5^B5?up1akjEPhi1-KUsK|J9XEBAbt%^F`t0I- zjRYYKI4OB7Zq3FqJFBZwbI=RuT~J|4tA8x)(v2yB^^+TYYJS>Et`_&yge##PuQ%0I z^|X!Vtof}`UuIxPjoH8kofw4u1pT5h`Ip}d8;l>WcG^qTe>@x63s#zoJiGmDM@_h= zo;8IZR`@AJRLnBNtatipUvL^(1P_a;q8P%&voqy#R!0(bNBTlV&*W9QU?kRV1B*~I zWvI?SNo2cB<7bgVY{F_CF$7z!02Qxfw-Ew#p!8PC#! z1sRfOl`d-Y@&=)l(Sl4CS=>fVvor5lYm61C!!iF3NMocKQHUYr0%QM}a4v2>rzPfM zUO}YRDb7-NEqW+p_;e0{Zi%0C$&B3CKx6|4BW`@`AwsxE?Vu}@Jm<3%T5O&05z+Yq zkK!QF(vlN}Rm}m_J+*W4`8i~R&`P0&5!;^@S#>7qkfb9wxFv@(wN@$k%2*sEwen$a zQnWymf+#Uyv)0lQVd?L1gpS}jMQZ(NHHCKRyu zjK|Zai0|N_)5iv)67(zDBCK4Ktm#ygP|0(m5tU`*AzR&{TSeSY8W=v5^=Ic`ahxM-LBWO+uoL~wxZmgcSJMUF9q%<%>jsvh9Dnp^_e>J_V=ySx4p?SF0Y zg4ZpZt@!h>WR76~P3_YchYOak7oOzR|`t+h!BbN}?zd zq+vMTt0!duALNWDwWVIA$O=%{lWJEj;5(QD()huhFL5=6x_=1h|5ESMW&S|*oxgF# z-0GRIb ziolwI13hJ-Rl(4Rj@*^=&Zz3vD$RX8bFWvBM{niz(%?z0gWNh_vUvpBDoa>-N=P4c zbw-XEJ@txIbc<`wC883;&yE4ayVh>+N($SJ01m}fumz!#!aOg*;y4Hl{V{b;&ux3& zBEmSq2jQ7#IbVm3TPBw?2vVN z0wzj|Y6EBS(V%Pb+@OPkMvEKHW~%DZk#u|A18pZMmCrjWh%7J4Ph>vG61 zRBgJ6w^8dNRg2*=K$Wvh$t>$Q^SMaIX*UpBG)0bqcvY%*by=$EfZAy{ZOA#^tB(D( zh}T(SZgdTj?bG9u+G{Avs5Yr1x=f3k7%K|eJp^>BHK#~dsG<&+=`mM@>kQ-cAJ2k) zT+Ht5liXdc^(aMi9su~{pJUhe)!^U&qn%mV6PS%lye+Iw5F@Xv8E zdR4#?iz+R4--iiHDQmQWfNre=iofAbF~1oGTa1Ce?hId~W^kPuN(5vhNx++ZLkn?l zUA7L~{0x|qA%%%P=8+-Ck{&2$UHn#OQncFS@uUVuE39c9o~#hl)v#!$X(X*4ban2c z{buYr9!`H2;6n73n^W3Vg(!gdBV7$e#v3qubWALaUEAf@`ava{UTx%2~VVQbEE(*Q8_ zv#me9i+0=QnY)$IT+@3vP1l9Wrne+MlZNGO6|zUVG+v&lm7Xw3P*+gS6e#6mVx~(w zyuaXogGTw4!!&P3oZ1|4oc_sGEa&m3Jsqy^lzUdJ^y8RlvUjDmbC^NZ0AmO-c*&m( zSI%4P9f|s!B#073b>Eet`T@J;3qY!NrABuUaED6M^=s-Q^2oZS`jVzuA z>g&g$!Tc>`u-Q9PmKu0SLu-X(tZeZ<%7F+$j3qOOftaoXO5=4!+P!%Cx0rNU+@E~{ zxCclYb~G(Ci%o{}4PC(Bu>TyX9slm5A^2Yi$$kCq-M#Jl)a2W9L-bq5%@Pw^ zh*iuuAz`x6N_rJ1LZ7J^MU9~}RYh+EVIVP+-62u+7IC%1p@;xmmQ`dGCx$QpnIUtK z0`++;Ddz7{_R^~KDh%_yo8WM$IQhcNOALCIGC$3_PtUs?Y44@Osw;OZ()Lk=(H&Vc zXjkHt+^1@M|J%Q&?4>;%T-i%#h|Tb1u;pO5rKst8(Cv2!3U{TRXdm&>fWTJG)n*q&wQPjRzg%pS1RO9}U0*C6fhUi&f#qoV`1{U<&mWKS<$oVFW>{&*$6)r6Rx)F4W zdUL8Mm_qNk6ycFVkI5F?V+cYFUch$92|8O^-Z1JC94GU+Nuk zA#n3Z1q4<6zRiv%W5`NGk*Ym{#0E~IA6*)H-=RmfWIY%mEC0? zSih7uchi`9-WkF2@z1ev6J_N~u;d$QfSNLMgPVpHZoh9oH-8D*;EhoCr~*kJ<|-VD z_jklPveOxWZq40E!SV@0XXy+~Vfn!7nZ1GXsn~U$>#u0d*f?RL9!NMlz^qxYmz|xt zz6A&MUAV#eD%^GcP#@5}QH5e7AV`}(N2#(3xpc!7dDmgu7C3TpgX5Z|$%Vu8=&SQI zdxUk*XS-#C^-cM*O>k}WD5K81e2ayyRA)R&5>KT1QL!T!%@}fw{>BsF+-pzu>;7{g z^CCSWfH;YtJGT@+An0Ded#zM9>UEFOdR_Xq zS~!5R*{p1Whq62ynHo|n$4p7&d|bal{iGsxAY?opi3R${)Zt*8YyOU!$TWMYXF?|i zPXYr}wJp#EH;keSG5WYJ*(~oiu#GDR>C4%-HpIWr7v`W`lzQN-lb?*vpoit z8FqJ)`LC4w8fO8Fu}AYV`awF2NLMS4$f+?=KisU4P6@#+_t)5WDz@f*qE|NG0*hwO z&gv^k^kC6Fg;5>Gr`Q46C{6>3F(p0QukG6NM07rxa&?)_C*eyU(jtli>9Zh#eUb(y zt9NbC-bp0>^m?i`?$aJUyBmF`N0zQ% zvF_;vLVI{tq%Ji%u*8s2p4iBirv*uD(?t~PEz$CfxVa=@R z^HQu6-+I9w>a35kX!P)TfnJDD!)j8!%38(vWNe9vK0{k*`FS$ABZ`rdwfQe@IGDki zssfXnsa6teKXCZUTd^qhhhUZ}>GG_>F0~LG7*<*x;8e39nb-0Bka(l)%+QZ_IVy3q zcmm2uKO0p)9|HGxk*e_$mX2?->&-MXe`=Fz3FRTFfM!$_y}G?{F9jmNgD+L%R`jM1 zIP-kb=3Hlsb35Q&qo(%Ja(LwQj>~!GI|Hgq65J9^A!ibChYB3kxLn@&=#pr}BwON0Q=e5;#sF8GGGuzx6O}z%u3l?jlKF&8Y#lUA)Cs6ZiW8DgOk|q z=YBPAMsO7AoAhWgnSKae2I7%7*Xk>#AyLX-InyBO?OD_^2^nI4#;G|tBvg3C0ldO0 z*`$g(q^es4VqXH2t~0-u^m5cfK8eECh3Rb2h1kW%%^8A!+ya3OHLw$8kHorx4(vJO zAlVu$nC>D{7i?7xDg3116Y2e+)Zb4FPAdZaX}qA!WW{$d?u+sK(iIKqOE-YM zH7y^hkny24==(1;qEacfFU{W{xSXhffC&DJV&oqw`u~WAl@=HIel>KC-mLs2ggFld zsSm-03=Jd^XNDA4i$vKqJ|e|TBc19bglw{)QL${Q(xlN?E;lPumO~;4w_McND6d+R zsc2p*&uRWd`wTDszTcWKiii1mNBrF7n&LQp$2Z<}zkv=8k2s6-^+#siy_K1`5R+n( z++5VOU^LDo(kt3ok?@$3drI`<%+SWcF*`CUWqAJxl3PAq!X|q{al;8%HfgxxM#2Vb zeBS756iU|BzB>bN2NP=AX&!{uZXS;|F`LLd9F^97UTMnNks_t7EPnjZF`2ocD2*u+ z?oKP{xXrD*AKGYGkZtlnvCuazg6g16ZAF{Nu%w+LCZ+v_*`0R$NK)tOh_c#cze;o$ z)kY(eZ5Viv<5zl1XfL(#GO|2FlXL#w3T?hpj3BZ&OAl^L!7@ zy;+iJWYQYP?$(`li_!|bfn!h~k#=v-#XXyjTLd+_txOqZZETqSEp>m+O0ji7MxZ*W zSdq+yqEmafrsLErZG8&;kH2kbCwluSa<@1yU3^Q#5HmW(hYVR0E6!4ZvH;Cr<$`qf zSvqRc`Pq_9b+xrtN3qLmds9;d7HdtlR!2NV$rZPCh6>(7f7M}>C^LeM_5^b$B~mn| z#)?`E=zeo9(9?{O_ko>51~h|c?8{F=2=_-o(-eRc z9p)o51krhCmff^U2oUi#$AG2p-*wSq8DZ(i!Jmu1wzD*)#%J&r)yZTq`3e|v4>EI- z=c|^$Qhv}lEyG@!{G~@}Wbx~vxTxwKoe9zn%5_Z^H$F1?JG_Kadc(G8#|@yaf2-4< zM1bdQF$b5R!W1f`j(S>Id;CHMzfpyjYEC_95VQ*$U3y5piVy=9Rdwg7g&)%#6;U%b2W}_VVdh}qPnM4FY9zFP(5eR zWuCEFox6e;COjs$1RV}IbpE0EV;}5IP}Oq|zcb*77PEDIZU{;@_;8*22{~JRvG~1t zc+ln^I+)Q*+Ha>(@=ra&L&a-kD;l$WEN;YL0q^GE8+})U_A_StHjX_gO{)N>tx4&F zRK?99!6JqktfeS-IsD@74yuq*aFJoV{5&K(W`6Oa2Qy0O5JG>O`zZ-p7vBGh!MxS;}}h6(96Wp`dci3DY?|B@1p8fVsDf$|0S zfE{WL5g3<9&{~yygYyR?jK!>;eZ2L#tpL2)H#89*b zycE?VViXbH7M}m33{#tI69PUPD=r)EVPTBku={Qh{ zKi*pht1jJ+yRhVE)1=Y()iS9j`FesMo$bjLSqPMF-i<42Hxl6%y7{#vw5YT(C}x0? z$rJU7fFmoiR&%b|Y*pG?7O&+Jb#Z%S8&%o~fc?S9c`Dwdnc4BJC7njo7?3bp#Yonz zPC>y`DVK~nzN^n}jB5RhE4N>LzhCZD#WQseohYXvqp5^%Ns!q^B z&8zQN(jgPS(2ty~g2t9!x9;Dao~lYVujG-QEq{vZp<1Nlp;oj#kFVsBnJssU^p-4% zKF_A?5sRmA>d*~^og-I95z$>T*K*33TGBPzs{OMoV2i+(P6K|95UwSj$Zn<@Rt(g%|iY z$SkSjYVJ)I<@S(kMQ6md{HxAa8S`^lXGV?ktLX!ngTVI~%WW+p#A#XTWaFWeBAl%U z&rVhve#Yse*h4BC4nrq7A1n>Rlf^ErbOceJC`o#fyCu@H;y)`E#a#)w)3eg^{Hw&E7);N5*6V+z%olvLj zp^aJ4`h*4L4ij)K+uYvdpil(Z{EO@u{BcMI&}5{ephilI%zCkBhBMCvOQT#zp|!18 zuNl=idd81|{FpGkt%ty=$fnZnWXxem!t4x{ zat@68CPmac(xYaOIeF}@O1j8O?2jbR!KkMSuix;L8x?m01}|bS2=&gsjg^t2O|+0{ zlzfu5r5_l4)py8uPb5~NHPG>!lYVynw;;T-gk1Pl6PQ39Mwgd2O+iHDB397H)2grN zHwbd>8i%GY>Pfy7;y5X7AN>qGLZVH>N_ZuJZ-`z9UA> zfyb$nbmPqxyF2F;UW}7`Cu>SS%0W6h^Wq5e{PWAjxlh=#Fq+6SiPa-L*551SZKX&w zc9TkPv4eao?kqomkZ#X%tA{`UIvf|_=Y7p~mHZKqO>i_;q4PrwVtUDTk?M7NCssa?Y4uxYrsXj!+k@`Cxl;&{NLs*6!R<6k9$Bq z%grLhxJ#G_j~ytJpiND8neLfvD0+xu>wa$-%5v;4;RYYM66PUab)c9ruUm%d{^s{# zTBBY??@^foRv9H}iEf{w_J%rV<%T1wv^`)Jm#snLTIifjgRkX``x2wV(D6(=VTLL4 zI-o}&5WuwBl~(XSLIn5~{cGWorl#z+=(vXuBXC#lp}SdW=_)~8Z(Vv!#3h2@pdA3d z{cIPYK@Ojc9(ph=H3T7;aY>(S3~iuIn05Puh^32WObj%hVN(Y{Ty?n?Cm#!kGNZFa zW6Ybz!tq|@erhtMo4xAus|H8V_c+XfE5mu|lYe|{$V3mKnb1~fqoFim;&_ZHN_=?t zysQwC4qO}rTi}k8_f=R&i27RdBB)@bTeV9Wcd}Rysvod}7I%ujwYbTI*cN7Kbp_hO z=eU521!#cx$0O@k9b$;pnCTRtLIzv){nVW6Ux1<0@te6`S5%Ew3{Z^9=lbL5$NFvd4eUtK?%zgmB;_I&p`)YtpN`2Im(?jPN<(7Ua_ZWJRF(CChv`(gHfWodK%+joy>8Vaa;H1w zIJ?!kA|x7V;4U1BNr(UrhfvjPii7YENLIm`LtnL9Sx z5E9TYaILoB2nSwDe|BVmrpLT43*dJ8;T@1l zJE)4LEzIE{IN}+Nvpo3=ZtV!U#D;rB@9OXYw^4QH+(52&pQEcZq&~u9bTg63ikW9! z=!_RjN2xO=F+bk>fSPhsjQA;)%M1My#34T`I7tUf>Q_L>DRa=>Eo(sapm>}}LUsN% zVw!C~a)xcca`G#g*Xqo>_uCJTz>LoWGSKOwp-tv`yvfqw{17t`9Z}U4o+q2JGP^&9 z(m}|d13XhYSnEm$_8vH-Lq$A^>oWUz1)bnv|AVn_0FwM$vYu&8+qUg$+qP}nwrykD zwmIF?wr$()X@33oz1@B9zi+?Th^nZnsES)rb@O*K^JL~ZH|pRRk$i0+ohh?Il)y&~ zQaq{}9YxPt5~_2|+r#{k#~SUhO6yFq)uBGtYMMg4h1qddg!`TGHocYROyNFJtYjNe z3oezNpq6%TP5V1g(?^5DMeKV|i6vdBq)aGJ)BRv;K(EL0_q7$h@s?BV$)w31*c(jd z{@hDGl3QdXxS=#?0y3KmPd4JL(q(>0ikTk6nt98ptq$6_M|qrPi)N>HY>wKFbnCKY z%0`~`9p)MDESQJ#A`_>@iL7qOCmCJ(p^>f+zqaMuDRk!z01Nd2A_W^D%~M73jTqC* zKu8u$$r({vP~TE8rPk?8RSjlRvG*BLF}ye~Su%s~rivmjg2F z24dhh6-1EQF(c>Z1E8DWY)Jw#9U#wR<@6J)3hjA&2qN$X%piJ4s={|>d-|Gzl~RNu z##iR(m;9TN3|zh+>HgTI&82iR>$YVoOq$a(2%l*2mNP(AsV=lR^>=tIP-R9Tw!BYnZROx`PN*JiNH>8bG}&@h0_v$yOTk#@1;Mh;-={ZU7e@JE(~@@y0AuETvsqQV@7hbKe2wiWk@QvV=Kz`%@$rN z_0Hadkl?7oEdp5eaaMqBm;#Xj^`fxNO^GQ9S3|Fb#%{lN;1b`~yxLGEcy8~!cz{!! z=7tS!I)Qq%w(t9sTSMWNhoV#f=l5+a{a=}--?S!rA0w}QF!_Eq>V4NbmYKV&^OndM z4WiLbqeC5+P@g_!_rs01AY6HwF7)$~%Ok^(NPD9I@fn5I?f$(rcOQjP+z?_|V0DiN zb}l0fy*el9E3Q7fVRKw$EIlb&T0fG~fDJZL7Qn8*a5{)vUblM)*)NTLf1ll$ zpQ^(0pkSTol`|t~`Y4wzl;%NRn>689mpQrW=SJ*rB;7}w zVHB?&sVa2%-q@ANA~v)FXb`?Nz8M1rHKiZB4xC9<{Q3T!XaS#fEk=sXI4IFMnlRqG+yaFw< zF{}7tcMjV04!-_FFD8(FtuOZx+|CjF@-xl6-{qSFF!r7L3yD()=*Ss6fT?lDhy(h$ zt#%F575$U(3-e2LsJd>ksuUZZ%=c}2dWvu8f!V%>z3gajZ!Dlk zm=0|(wKY`c?r$|pX6XVo6padb9{EH}px)jIsdHoqG^(XH(7}r^bRa8BC(%M+wtcB? z6G2%tui|Tx6C3*#RFgNZi9emm*v~txI}~xV4C`Ns)qEoczZ>j*r zqQCa5k90Gntl?EX!{iWh=1t$~jVoXjs&*jKu0Ay`^k)hC^v_y0xU~brMZ6PPcmt5$ z@_h`f#qnI$6BD(`#IR0PrITIV^~O{uo=)+Bi$oHA$G* zH0a^PRoeYD3jU_k%!rTFh)v#@cq`P3_y=6D(M~GBud;4 zCk$LuxPgJ5=8OEDlnU!R^4QDM4jGni}~C zy;t2E%Qy;A^bz_5HSb5pq{x{g59U!ReE?6ULOw58DJcJy;H?g*ofr(X7+8wF;*3{rx>j&27Syl6A~{|w{pHb zeFgu0E>OC81~6a9(2F13r7NZDGdQxR8T68&t`-BK zE>ZV0*0Ba9HkF_(AwfAds-r=|dA&p`G&B_zn5f9Zfrz9n#Rvso`x%u~SwE4SzYj!G zVQ0@jrLwbYP=awX$21Aq!I%M{x?|C`narFWhp4n;=>Sj!0_J!k7|A0;N4!+z%Oqlk z1>l=MHhw3bi1vT}1!}zR=6JOIYSm==qEN#7_fVsht?7SFCj=*2+Ro}B4}HR=D%%)F z?eHy=I#Qx(vvx)@Fc3?MT_@D))w@oOCRR5zRw7614#?(-nC?RH`r(bb{Zzn+VV0bm zJ93!(bfrDH;^p=IZkCH73f*GR8nDKoBo|!}($3^s*hV$c45Zu>6QCV(JhBW=3(Tpf z=4PT6@|s1Uz+U=zJXil3K(N6;ePhAJhCIo`%XDJYW@x#7Za);~`ANTvi$N4(Fy!K- z?CQ3KeEK64F0@ykv$-0oWCWhYI-5ZC1pDqui@B|+LVJmU`WJ=&C|{I_))TlREOc4* zSd%N=pJ_5$G5d^3XK+yj2UZasg2) zXMLtMp<5XWWfh-o@ywb*nCnGdK{&S{YI54Wh2|h}yZ})+NCM;~i9H@1GMCgYf`d5n zwOR(*EEkE4-V#R2+Rc>@cAEho+GAS2L!tzisLl${42Y=A7v}h;#@71_Gh2MV=hPr0_a% z0!={Fcv5^GwuEU^5rD|sP;+y<%5o9;#m>ssbtVR2g<420(I-@fSqfBVMv z?`>61-^q;M(b3r2z{=QxSjyH=-%99fpvb}8z}d;%_8$$J$qJg1Sp3KzlO_!nCn|g8 zzg8skdHNsfgkf8A7PWs;YBz_S$S%!hWQ@G>guCgS--P!!Ui9#%GQ#Jh?s!U-4)7ozR?i>JXHU$| zg0^vuti{!=N|kWorZNFX`dJgdphgic#(8sOBHQdBkY}Qzp3V%T{DFb{nGPgS;QwnH9B9;-Xhy{? z(QVwtzkn9I)vHEmjY!T3ifk1l5B?%%TgP#;CqG-?16lTz;S_mHOzu#MY0w}XuF{lk z*dt`2?&plYn(B>FFXo+fd&CS3q^hquSLVEn6TMAZ6e*WC{Q2e&U7l|)*W;^4l~|Q= zt+yFlLVqPz!I40}NHv zE2t1meCuGH%<`5iJ(~8ji#VD{?uhP%F(TnG#uRZW-V}1=N%ev&+Gd4v!0(f`2Ar-Y z)GO6eYj7S{T_vxV?5^%l6TF{ygS_9e2DXT>9caP~xq*~oE<5KkngGtsv)sdCC zaQH#kSL%c*gLj6tV)zE6SGq|0iX*DPV|I`byc9kn_tNQkPU%y<`rj zMC}lD<93=Oj+D6Y2GNMZb|m$^)RVdi`&0*}mxNy0BW#0iq!GGN2BGx5I0LS>I|4op z(6^xWULBr=QRpbxIJDK~?h;K#>LwQI4N<8V?%3>9I5l+e*yG zFOZTIM0c3(q?y9f7qDHKX|%zsUF%2zN9jDa7%AK*qrI5@z~IruFP+IJy7!s~TE%V3 z_PSSxXlr!FU|Za>G_JL>DD3KVZ7u&}6VWbwWmSg?5;MabycEB)JT(eK8wg`^wvw!Q zH5h24_E$2cuib&9>Ue&@%Cly}6YZN-oO_ei5#33VvqV%L*~ZehqMe;)m;$9)$HBsM zfJ96Hk8GJyWwQ0$iiGjwhxGgQX$sN8ij%XJzW`pxqgwW=79hgMOMnC|0Q@ed%Y~=_ z?OnjUB|5rS+R$Q-p)vvM(eFS+Qr{_w$?#Y;0Iknw3u(+wA=2?gPyl~NyYa3me{-Su zhH#8;01jEm%r#5g5oy-f&F>VA5TE_9=a0aO4!|gJpu470WIrfGo~v}HkF91m6qEG2 zK4j=7C?wWUMG$kYbIp^+@)<#ArZ$3k^EQxraLk0qav9TynuE7T79%MsBxl3|nRn?L zD&8kt6*RJB6*a7=5c57wp!pg)p6O?WHQarI{o9@3a32zQ3FH8cK@P!DZ?CPN_LtmC6U4F zlv8T2?sau&+(i@EL6+tvP^&=|aq3@QgL4 zOu6S3wSWeYtgCnKqg*H4ifIQlR4hd^n{F+3>h3;u_q~qw-Sh;4dYtp^VYymX12$`? z;V2_NiRt82RC=yC+aG?=t&a81!gso$hQUb)LM2D4Z{)S zI1S9f020mSm(Dn$&Rlj0UX}H@ zv={G+fFC>Sad0~8yB%62V(NB4Z|b%6%Co8j!>D(VyAvjFBP%gB+`b*&KnJ zU8s}&F+?iFKE(AT913mq;57|)q?ZrA&8YD3Hw*$yhkm;p5G6PNiO3VdFlnH-&U#JH zEX+y>hB(4$R<6k|pt0?$?8l@zeWk&1Y5tlbgs3540F>A@@rfvY;KdnVncEh@N6Mfi zY)8tFRY~Z?Qw!{@{sE~vQy)0&fKsJpj?yR`Yj+H5SDO1PBId3~d!yjh>FcI#Ug|^M z7-%>aeyQhL8Zmj1!O0D7A2pZE-$>+-6m<#`QX8(n)Fg>}l404xFmPR~at%$(h$hYD zoTzbxo`O{S{E}s8Mv6WviXMP}(YPZoL11xfd>bggPx;#&pFd;*#Yx%TtN1cp)MuHf z+Z*5CG_AFPwk624V9@&aL0;=@Ql=2h6aJoqWx|hPQQzdF{e7|fe(m){0==hk_!$ou zI|p_?kzdO9&d^GBS1u+$>JE-6Ov*o{mu@MF-?$r9V>i%;>>Fo~U`ac2hD*X}-gx*v z1&;@ey`rA0qNcD9-5;3_K&jg|qvn@m^+t?8(GTF0l#|({Zwp^5Ywik@bW9mN+5`MU zJ#_Ju|jtsq{tv)xA zY$5SnHgHj}c%qlQG72VS_(OSv;H~1GLUAegygT3T-J{<#h}))pk$FjfRQ+Kr%`2ZiI)@$96Nivh82#K@t>ze^H?R8wHii6Pxy z0o#T(lh=V>ZD6EXf0U}sG~nQ1dFI`bx;vivBkYSVkxXn?yx1aGxbUiNBawMGad;6? zm{zp?xqAoogt=I2H0g@826=7z^DmTTLB11byYvAO;ir|O0xmNN3Ec0w%yHO({-%q(go%?_X{LP?=E1uXoQgrEGOfL1?~ zI%uPHC23dn-RC@UPs;mxq6cFr{UrgG@e3ONEL^SoxFm%kE^LBhe_D6+Ia+u0J=)BC zf8FB!0J$dYg33jb2SxfmkB|8qeN&De!%r5|@H@GiqReK(YEpnXC;-v~*o<#JmYuze zW}p-K=9?0=*fZyYTE7A}?QR6}m_vMPK!r~y*6%My)d;x4R?-=~MMLC_02KejX9q6= z4sUB4AD0+H4ulSYz4;6mL8uaD07eXFvpy*i5X@dmx--+9`ur@rcJ5<L#s%nq3MRi4Dpr;#28}dl36M{MkVs4+Fm3Pjo5qSV)h}i(2^$Ty|<7N z>*LiBzFKH30D!$@n^3B@HYI_V1?yM(G$2Ml{oZ}?frfPU+{i|dHQOP^M0N2#NN_$+ zs*E=MXUOd=$Z2F4jSA^XIW=?KN=w6{_vJ4f(ZYhLxvFtPozPJv9k%7+z!Zj+_0|HC zMU0(8`8c`Sa=%e$|Mu2+CT22Ifbac@7Vn*he`|6Bl81j`44IRcTu8aw_Y%;I$Hnyd zdWz~I!tkWuGZx4Yjof(?jM;exFlUsrj5qO=@2F;56&^gM9D^ZUQ!6TMMUw19zslEu zwB^^D&nG96Y+Qwbvgk?Zmkn9%d{+V;DGKmBE(yBWX6H#wbaAm&O1U^ zS4YS7j2!1LDC6|>cfdQa`}_^satOz6vc$BfFIG07LoU^IhVMS_u+N=|QCJao0{F>p z-^UkM)ODJW9#9*o;?LPCRV1y~k9B`&U)jbTdvuxG&2%!n_Z&udT=0mb@e;tZ$_l3bj6d0K2;Ya!&)q`A${SmdG_*4WfjubB)Mn+vaLV+)L5$yD zYSTGxpVok&fJDG9iS8#oMN{vQneO|W{Y_xL2Hhb%YhQJgq7j~X7?bcA|B||C?R=Eo z!z;=sSeKiw4mM$Qm>|aIP3nw36Tbh6Eml?hL#&PlR5xf9^vQGN6J8op1dpLfwFg}p zlqYx$610Zf?=vCbB_^~~(e4IMic7C}X(L6~AjDp^;|=d$`=!gd%iwCi5E9<6Y~z0! zX8p$qprEadiMgq>gZ_V~n$d~YUqqqsL#BE6t9ufXIUrs@DCTfGg^-Yh5Ms(wD1xAf zTX8g52V!jr9TlWLl+whcUDv?Rc~JmYs3haeG*UnV;4bI=;__i?OSk)bF3=c9;qTdP zeW1exJwD+;Q3yAw9j_42Zj9nuvs%qGF=6I@($2Ue(a9QGRMZTd4ZAlxbT5W~7(alP1u<^YY!c3B7QV z@jm$vn34XnA6Gh1I)NBgTmgmR=O1PKp#dT*mYDPRZ=}~X3B8}H*e_;;BHlr$FO}Eq zJ9oWk0y#h;N1~ho724x~d)A4Z-{V%F6#e5?Z^(`GGC}sYp5%DKnnB+i-NWxwL-CuF+^JWNl`t@VbXZ{K3#aIX+h9-{T*+t(b0BM&MymW9AA*{p^&-9 zWpWQ?*z(Yw!y%AoeoYS|E!(3IlLksr@?Z9Hqlig?Q4|cGe;0rg#FC}tXTmTNfpE}; z$sfUYEG@hLHUb$(K{A{R%~%6MQN|Bu949`f#H6YC*E(p3lBBKcx z-~Bsd6^QsKzB0)$FteBf*b3i7CN4hccSa-&lfQz4qHm>eC|_X!_E#?=`M(bZ{$cvU zZpMbr|4omp`s9mrgz@>4=Fk3~8Y7q$G{T@?oE0<(I91_t+U}xYlT{c&6}zPAE8ikT z3DP!l#>}i!A(eGT+@;fWdK#(~CTkwjs?*i4SJVBuNB2$6!bCRmcm6AnpHHvnN8G<| zuh4YCYC%5}Zo;BO1>L0hQ8p>}tRVx~O89!${_NXhT!HUoGj0}bLvL2)qRNt|g*q~B z7U&U7E+8Ixy1U`QT^&W@ZSRN|`_Ko$-Mk^^c%`YzhF(KY9l5))1jSyz$&>mWJHZzHt0Jje%BQFxEV}C00{|qo5_Hz7c!FlJ|T(JD^0*yjkDm zL}4S%JU(mBV|3G2jVWU>DX413;d+h0C3{g3v|U8cUj`tZL37Sf@1d*jpwt4^B)`bK zZdlwnPB6jfc7rIKsldW81$C$a9BukX%=V}yPnaBz|i6(h>S)+Bn44@i8RtBZf0XetH&kAb?iAL zD%Ge{>Jo3sy2hgrD?15PM}X_)(6$LV`&t*D`IP)m}bzM)+x-xRJ zavhA)>hu2cD;LUTvN38FEtB94ee|~lIvk~3MBPzmTsN|7V}Kzi!h&za#NyY zX^0BnB+lfBuW!oR#8G&S#Er2bCVtA@5FI`Q+a-e?G)LhzW_chWN-ZQmjtR

eWu-UOPu^G}|k=o=;ffg>8|Z*qev7qS&oqA7%Z{4Ezb!t$f3& z^NuT8CSNp`VHScyikB1YO{BgaBVJR&>dNIEEBwYkfOkWN;(I8CJ|vIfD}STN z{097)R9iC@6($s$#dsb*4BXBx7 zb{6S2O}QUk>upEfij9C2tjqWy7%%V@Xfpe)vo6}PG+hmuY1Tc}peynUJLLmm)8pshG zb}HWl^|sOPtYk)CD-7{L+l(=F zOp}fX8)|n{JDa&9uI!*@jh^^9qP&SbZ(xxDhR)y|bjnn|K3MeR3gl6xcvh9uqzb#K zYkVjnK$;lUky~??mcqN-)d5~mk{wXhrf^<)!Jjqc zG~hX0P_@KvOKwV=X9H&KR3GnP3U)DfqafBt$e10}iuVRFBXx@uBQ)sn0J%%c<;R+! zQz;ETTVa+ma>+VF%U43w?_F6s0=x@N2(oisjA7LUOM<$|6iE|$WcO67W|KY8JUV_# zg7P9K3Yo-c*;EmbsqT!M4(WT`%9uk+s9Em-yB0bE{B%F4X<8fT!%4??vezaJ(wJhj zfOb%wKfkY3RU}7^FRq`UEbB-#A-%7)NJQwQd1As=!$u#~2vQ*CE~qp`u=_kL<`{OL zk>753UqJVx1-4~+d@(pnX-i zV4&=eRWbJ)9YEGMV53poXpv$vd@^yd05z$$@i5J7%>gYKBx?mR2qGv&BPn!tE-_aW zg*C!Z&!B zH>3J16dTJC(@M0*kIc}Jn}jf=f*agba|!HVm|^@+7A?V>Woo!$SJko*Jv1mu>;d}z z^vF{3u5Mvo_94`4kq2&R2`32oyoWc2lJco3`Ls0Ew4E7*AdiMbn^LCV%7%mU)hr4S3UVJjDLUoIKRQ)gm?^{1Z}OYzd$1?a~tEY ztjXmIM*2_qC|OC{7V%430T?RsY?ZLN$w!bkDOQ0}wiq69){Kdu3SqW?NMC))S}zq^ zu)w!>E1!;OrXO!RmT?m&PA;YKUjJy5-Seu=@o;m4*Vp$0OipBl4~Ub)1xBdWkZ47=UkJd$`Z}O8ZbpGN$i_WtY^00`S8=EHG#Ff{&MU1L(^wYjTchB zMTK%1LZ(eLLP($0UR2JVLaL|C2~IFbWirNjp|^=Fl48~Sp9zNOCZ@t&;;^avfN(NpNfq}~VYA{q%yjHo4D>JB>XEv(~Z!`1~SoY=9v zTq;hrjObE_h)cmHXLJ>LC_&XQ2BgGfV}e#v}ZF}iF97bG`Nog&O+SA`2zsn%bbB309}I$ zYi;vW$k@fC^muYBL?XB#CBuhC&^H)F4E&vw(5Q^PF{7~}(b&lF4^%DQzL0(BVk?lM zTHXTo4?Ps|dRICEiux#y77_RF8?5!1D-*h5UY&gRY`WO|V`xxB{f{DHzBwvt1W==r zdfAUyd({^*>Y7lObr;_fO zxDDw7X^dO`n!PLqHZ`by0h#BJ-@bAFPs{yJQ~Ylj^M5zWsxO_WFHG}8hH>OK{Q)9` zSRP94d{AM(q-2x0yhK@aNMv!qGA5@~2tB;X?l{Pf?DM5Y*QK`{mGA? zjx;gwnR~#Nep12dFk<^@-U{`&`P1Z}Z3T2~m8^J&7y}GaMElsTXg|GqfF3>E#HG=j zMt;6hfbfjHSQ&pN9(AT8q$FLKXo`N(WNHDY!K6;JrHZCO&ISBdX`g8sXvIf?|8 zX$-W^ut!FhBxY|+R49o44IgWHt}$1BuE|6|kvn1OR#zhyrw}4H*~cpmFk%K(CTGYc zNkJ8L$eS;UYDa=ZHWZy`rO`!w0oIcgZnK&xC|93#nHvfb^n1xgxf{$LB`H1ao+OGb zKG_}>N-RHSqL(RBdlc7J-Z$Gaay`wEGJ_u-lo88{`aQ*+T~+x(H5j?Q{uRA~>2R+} zB+{wM2m?$->unwg8-GaFrG%ZmoHEceOj{W21)Mi2lAfT)EQuNVo+Do%nHPuq7Ttt7 z%^6J5Yo64dH671tOUrA7I2hL@HKZq;S#Ejxt;*m-l*pPj?=i`=E~FAXAb#QH+a}-% z#3u^pFlg%p{hGiIp>05T$RiE*V7bPXtkz(G<+^E}Risi6F!R~Mbf(Qz*<@2&F#vDr zaL#!8!&ughWxjA(o9xtK{BzzYwm_z2t*c>2jI)c0-xo8ahnEqZ&K;8uF*!Hg0?Gd* z=eJK`FkAr>7$_i$;kq3Ks5NNJkNBnw|1f-&Ys56c9Y@tdM3VTTuXOCbWqye9va6+ZSeF0eh} zYb^ct&4lQTfNZ3M3(9?{;s><(zq%hza7zcxlZ+`F8J*>%4wq8s$cC6Z=F@ zhbvdv;n$%vEI$B~B)Q&LkTse!8Vt};7Szv2@YB!_Ztp@JA>rc(#R1`EZcIdE+JiI% zC2!hgYt+~@%xU?;ir+g92W`*j z3`@S;I6@2rO28zqj&SWO^CvA5MeNEhBF+8-U0O0Q1Co=I^WvPl%#}UFDMBVl z5iXV@d|`QTa$>iw;m$^}6JeuW zjr;{)S2TfK0Q%xgHvONSJb#NA|LOmg{U=k;R?&1tQbylMEY4<1*9mJh&(qo`G#9{X zYRs)#*PtEHnO;PV0G~6G`ca%tpKgb6<@)xc^SQY58lTo*S$*sv5w7bG+8YLKYU`8{ zNBVlvgaDu7icvyf;N&%42z2L4(rR<*Jd48X8Jnw zN>!R$%MZ@~Xu9jH?$2Se&I|ZcW>!26BJP?H7og0hT(S`nXh6{sR36O^7%v=31T+eL z)~BeC)15v>1m#(LN>OEwYFG?TE0_z)MrT%3SkMBBjvCd6!uD+03Jz#!s#Y~b1jf>S z&Rz5&8rbLj5!Y;(Hx|UY(2aw~W(8!3q3D}LRE%XX(@h5TnP@PhDoLVQx;6|r^+Bvs zaR55cR%Db9hZ<<|I%dDkone+8Sq7dqPOMnGoHk~-R*#a8w$c)`>4U`k+o?2|E>Sd4 zZ0ZVT{95pY$qKJ54K}3JB!(WcES>F+x56oJBRg))tMJ^#Qc(2rVcd5add=Us6vpBNkIg9b#ulk%!XBU zV^fH1uY(rGIAiFew|z#MM!qsVv%ZNb#why9%9In4Kj-hDYtMdirWLFzn~de!nnH(V zv0>I3;X#N)bo1$dFzqo(tzmvqNUKraAz~?)OSv42MeM!OYu;2VKn2-s7#fucX`|l~ zplxtG1Pgk#(;V=`P_PZ`MV{Bt4$a7;aLvG@KQo%E=;7ZO&Ws-r@XL+AhnPn>PAKc7 zQ_iQ4mXa-a4)QS>cJzt_j;AjuVCp8g^|dIV=DI0>v-f_|w5YWAX61lNBjZEZax3aV znher(j)f+a9_s8n#|u=kj0(unR1P-*L7`{F28xv054|#DMh}q=@rs@-fbyf(2+52L zN>hn3v!I~%jfOV=j(@xLOsl$Jv-+yR5{3pX)$rIdDarl7(C3)})P`QoHN|y<<2n;` zJ0UrF=Zv}d=F(Uj}~Yv9(@1pqUSRa5_bB*AvQ|Z-6YZ*N%p(U z<;Bpqr9iEBe^LFF!t{1UnRtaH-9=@p35fMQJ~1^&)(2D|^&z?m z855r&diVS6}jmt2)A7LZDiv;&Ys6@W5P{JHY!!n7W zvj3(2{1R9Y=TJ|{^2DK&be*ZaMiRHw>WVI^701fC) zAp1?8?oiU%Faj?Qhou6S^d11_7@tEK-XQ~%q!!7hha-Im^>NcRF7OH7s{IO7arZQ{ zE8n?2><7*!*lH}~usWPWZ}2&M+)VQo7C!AWJSQc>8g_r-P`N&uybK5)p$5_o;+58Q z-Ux2l<3i|hxqqur*qAfHq=)?GDchq}ShV#m6&w|mi~ar~`EO_S=fb~<}66U>5i7$H#m~wR;L~4yHL2R&;L*u7-SPdHxLS&Iy76q$2j#Pe)$WulRiCICG*t+ zeehM8`!{**KRL{Q{8WCEFLXu3+`-XF(b?c1Z~wg?c0lD!21y?NLq?O$STk3NzmrHM zsCgQS5I+nxDH0iyU;KKjzS24GJmG?{D`08|N-v+Egy92lBku)fnAM<}tELA_U`)xKYb=pq|hejMCT1-rg0Edt6(*E9l9WCKI1a=@c99swp2t6Tx zFHy`8Hb#iXS(8c>F~({`NV@F4w0lu5X;MH6I$&|h*qfx{~DJ*h5e|61t1QP}tZEIcjC%!Fa)omJTfpX%aI+OD*Y(l|xc0$1Zip;4rx; zV=qI!5tSuXG7h?jLR)pBEx!B15HCoVycD&Z2dlqN*MFQDb!|yi0j~JciNC!>){~ zQQgmZvc}0l$XB0VIWdg&ShDTbTkArryp3x)T8%ulR;Z?6APx{JZyUm=LC-ACkFm`6 z(x7zm5ULIU-xGi*V6x|eF~CN`PUM%`!4S;Uv_J>b#&OT9IT=jx5#nydC4=0htcDme zDUH*Hk-`Jsa>&Z<7zJ{K4AZE1BVW%zk&MZ^lHyj8mWmk|Pq8WwHROz0Kwj-AFqvR)H2gDN*6dzVk>R3@_CV zw3Z@6s^73xW)XY->AFwUlk^4Q=hXE;ckW=|RcZFchyOM0vqBW{2l*QR#v^SZNnT6j zZv|?ZO1-C_wLWVuYORQryj29JA; zS4BsxfVl@X!W{!2GkG9fL4}58Srv{$-GYngg>JuHz!7ZPQbfIQr4@6ZC4T$`;Vr@t zD#-uJ8A!kSM*gA&^6yWi|F}&59^*Rx{qn3z{(JYxrzg!X2b#uGd>&O0e=0k_2*N?3 zYXV{v={ONL{rW~z_FtFj7kSSJZ?s);LL@W&aND7blR8rlvkAb48RwJZlOHA~t~RfC zOD%ZcOzhYEV&s9%qns0&ste5U!^MFWYn`Od()5RwIz6%@Ek+Pn`s79unJY-$7n-Uf z&eUYvtd)f7h7zG_hDiFC!psCg#q&0c=GHKOik~$$>$Fw*k z;G)HS$IR)Cu72HH|JjeeauX;U6IgZ_IfxFCE_bGPAU25$!j8Etsl0Rk@R`$jXuHo8 z3Hhj-rTR$Gq(x)4Tu6;6rHQhoCvL4Q+h0Y+@Zdt=KTb0~wj7-(Z9G%J+aQu05@k6JHeCC|YRFWGdDCV}ja;-yl^9<`>f=AwOqML1a~* z9@cQYb?!+Fmkf}9VQrL8$uyq8k(r8)#;##xG9lJ-B)Fg@15&To(@xgk9SP*bkHlxiy8I*wJQylh(+9X~H-Is!g&C!q*eIYuhl&fS&|w)dAzXBdGJ&Mp$+8D| zZaD<+RtjI90QT{R0YLk6_dm=GfCg>7;$ zlyLsNYf@MfLH<}ott5)t2CXiQos zFLt^`%ygB2Vy^I$W3J_Rt4olRn~Gh}AW(`F@LsUN{d$sR%bU&3;rsD=2KCL+4c`zv zlI%D>9-)U&R3;>d1Vdd5b{DeR!HXDm44Vq*u?`wziLLsFUEp4El;*S0;I~D#TgG0s zBXYZS{o|Hy0A?LVNS)V4c_CFwyYj-E#)4SQq9yaf`Y2Yhk7yHSdos~|fImZG5_3~~o<@jTOH@Mc7`*xn-aO5F zyFT-|LBsm(NbWkL^oB-Nd31djBaYebhIGXhsJyn~`SQ6_4>{fqIjRp#Vb|~+Qi}Mdz!Zsw= zz?5L%F{c{;Cv3Q8ab>dsHp)z`DEKHf%e9sT(aE6$az?A}3P`Lm(~W$8Jr=;d8#?dm_cmv>2673NqAOenze z=&QW`?TQAu5~LzFLJvaJ zaBU3mQFtl5z?4XQDBWNPaH4y)McRpX#$(3o5Nx@hVoOYOL&-P+gqS1cQ~J;~1roGH zVzi46?FaI@w-MJ0Y7BuAg*3;D%?<_OGsB3)c|^s3A{UoAOLP8scn`!5?MFa|^cTvq z#%bYG3m3UO9(sH@LyK9-LSnlVcm#5^NRs9BXFtRN9kBY2mPO|@b7K#IH{B{=0W06) zl|s#cIYcreZ5p3j>@Ly@35wr-q8z5f9=R42IsII=->1stLo@Q%VooDvg@*K(H@*5g zUPS&cM~k4oqp`S+qp^*nxzm^0mg3h8ppEHQ@cXyQ=YKV-6)FB*$KCa{POe2^EHr{J zOxcVd)s3Mzs8m`iV?MSp=qV59blW9$+$P+2;PZDRUD~sr*CQUr&EDiCSfH@wuHez+ z`d5p(r;I7D@8>nbZ&DVhT6qe+accH;<}q$8Nzz|d1twqW?UV%FMP4Y@NQ`3(+5*i8 zP9*yIMP7frrneG3M9 zf>GsjA!O#Bifr5np-H~9lR(>#9vhE6W-r`EjjeQ_wdWp+rt{{L5t5t(Ho|4O24@}4 z_^=_CkbI`3;~sXTnnsv=^b3J}`;IYyvb1gM>#J9{$l#Zd*W!;meMn&yXO7x`Epx_Y zm-1wlu~@Ii_7D}>%tzlXW;zQT=uQXSG@t$<#6-W*^vy7Vr2TCpnix@7!_|aNXEnN<-m?Oq;DpN*x6f>w za1Wa5entFEDtA0SD%iZv#3{wl-S`0{{i3a9cmgNW`!TH{J*~{@|5f%CKy@uk*8~af zt_d34U4y&3y9IZ5cXxLQ?(XjH5?q3Z0KxK~y!-CUyWG6{<)5lkhbox0HnV&7^zNBn zjc|?X!Y=63(Vg>#&Wx%=LUr5{i@~OdzT#?P8xu#P*I_?Jl7xM4dq)4vi}3Wj_c=XI zSbc)@Q2Et4=(nBDU{aD(F&*%Ix!53_^0`+nOFk)}*34#b0Egffld|t_RV91}S0m)0 zap{cQDWzW$geKzYMcDZDAw480!1e1!1Onpv9fK9Ov~sfi!~OeXb(FW)wKx335nNY! za6*~K{k~=pw`~3z!Uq%?MMzSl#s%rZM{gzB7nB*A83XIGyNbi|H8X>a5i?}Rs+z^; z2iXrmK4|eDOu@{MdS+?@(!-Ar4P4?H_yjTEMqm7`rbV4P275(-#TW##v#Dt14Yn9UB-Sg3`WmL0+H~N;iC`Mg%pBl?1AAOfZ&e; z*G=dR>=h_Mz@i;lrGpIOQwezI=S=R8#);d*;G8I(39ZZGIpWU)y?qew(t!j23B9fD z?Uo?-Gx3}6r8u1fUy!u)7LthD2(}boE#uhO&mKBau8W8`XV7vO>zb^ZVWiH-DOjl2 zf~^o1CYVU8eBdmpAB=T%i(=y}!@3N%G-*{BT_|f=egqtucEtjRJJhSf)tiBhpPDpgzOpG12UgvOFnab&16Zn^2ZHjs)pbd&W1jpx%%EXmE^ zdn#R73^BHp3w%&v!0~azw(Fg*TT*~5#dJw%-UdxX&^^(~V&C4hBpc+bPcLRZizWlc zjR;$4X3Sw*Rp4-o+a4$cUmrz05RucTNoXRINYG*DPpzM&;d1GNHFiyl(_x#wspacQ zL)wVFXz2Rh0k5i>?Ao5zEVzT)R(4Pjmjv5pzPrav{T(bgr|CM4jH1wDp6z*_jnN{V ziN56m1T)PBp1%`OCFYcJJ+T09`=&=Y$Z#!0l0J2sIuGQtAr>dLfq5S;{XGJzNk@a^ zk^eHlC4Gch`t+ue3RviiOlhz81CD9z~d|n5;A>AGtkZMUQ#f>5M14f2d}2 z8<*LNZvYVob!p9lbmb!0jt)xn6O&JS)`}7v}j+csS3e;&Awj zoNyjnqLzC(QQ;!jvEYUTy73t_%16p)qMb?ihbU{y$i?=a7@JJoXS!#CE#y}PGMK~3 zeeqqmo7G-W_S97s2eed^erB2qeh4P25)RO1>MH7ai5cZJTEevogLNii=oKG)0(&f` z&hh8cO{of0;6KiNWZ6q$cO(1)9r{`}Q&%p*O0W7N--sw3Us;)EJgB)6iSOg(9p_mc zRw{M^qf|?rs2wGPtjVKTOMAfQ+ZNNkb$Ok0;Pe=dNc7__TPCzw^H$5J0l4D z%p(_0w(oLmn0)YDwrcFsc*8q)J@ORBRoZ54GkJpxSvnagp|8H5sxB|ZKirp%_mQt_ z81+*Y8{0Oy!r8Gmih48VuRPwoO$dDW@h53$C)duL4_(osryhwZSj%~KsZ?2n?b`Z* z#C8aMdZxYmCWSM{mFNw1ov*W}Dl=%GQpp90qgZ{(T}GOS8#>sbiEU;zYvA?=wbD5g+ahbd1#s`=| zV6&f#ofJC261~Ua6>0M$w?V1j##jh-lBJ2vQ%&z`7pO%frhLP-1l)wMs=3Q&?oth1 zefkPr@3Z(&OL@~|<0X-)?!AdK)ShtFJ;84G2(izo3cCuKc{>`+aDoziL z6gLTL(=RYeD7x^FYA%sPXswOKhVa4i(S4>h&mLvS##6-H?w8q!B<8Alk>nQEwUG)SFXK zETfcTwi=R3!ck|hSM`|-^N3NWLav&UTO{a9=&Tuz-Kq963;XaRFq#-1R18fi^Gb-; zVO>Q{Oe<^b0WA!hkBi9iJp3`kGwacXX2CVQ0xQn@Y2OhrM%e4)Ea7Y*Df$dY2BpbL zv$kX}*#`R1uNA(7lk_FAk~{~9Z*Si5xd(WKQdD&I?8Y^cK|9H&huMU1I(251D7(LL z+){kRc=ALmD;#SH#YJ+|7EJL6e~w!D7_IrK5Q=1DCulUcN(3j`+D_a|GP}?KYx}V+ zx_vLTYCLb0C?h;e<{K0`)-|-qfM16y{mnfX(GGs2H-;-lRMXyb@kiY^D;i1haxoEk zsQ7C_o2wv?;3KS_0w^G5#Qgf*>u)3bT<3kGQL-z#YiN9QH7<(oDdNlSdeHD zQJN-U*_wJM_cU}1YOH=m>DW~{%MAPxL;gLdU6S5xLb$gJt#4c2KYaEaL8ORWf=^(l z-2`8^J;&YG@vb9em%s~QpU)gG@24BQD69;*y&-#0NBkxumqg#YYomd2tyo0NGCr8N z5<5-E%utH?Ixt!(Y4x>zIz4R^9SABVMpLl(>oXnBNWs8w&xygh_e4*I$y_cVm?W-^ ze!9mPy^vTLRclXRGf$>g%Y{(#Bbm2xxr_Mrsvd7ci|X|`qGe5=54Zt2Tb)N zlykxE&re1ny+O7g#`6e_zyjVjRi5!DeTvSJ9^BJqQ*ovJ%?dkaQl!8r{F`@KuDEJB3#ho5 zmT$A&L=?}gF+!YACb=%Y@}8{SnhaGCHRmmuAh{LxAn0sg#R6P_^cJ-9)+-{YU@<^- zlYnH&^;mLVYE+tyjFj4gaAPCD4CnwP75BBXA`O*H(ULnYD!7K14C!kGL_&hak)udZ zkQN8)EAh&9I|TY~F{Z6mBv7sz3?<^o(#(NXGL898S3yZPTaT|CzZpZ~pK~*9Zcf2F zgwuG)jy^OTZD`|wf&bEdq4Vt$ir-+qM7BosXvu`>W1;iFN7yTvcpN_#at)Q4n+(Jh zYX1A-24l9H5jgY?wdEbW{(6U1=Kc?Utren80bP`K?J0+v@{-RDA7Y8yJYafdI<7-I z_XA!xeh#R4N7>rJ_?(VECa6iWhMJ$qdK0Ms27xG&$gLAy(|SO7_M|AH`fIY)1FGDp zlsLwIDshDU;*n`dF@8vV;B4~jRFpiHrJhQ6TcEm%OjWTi+KmE7+X{19 z>e!sg0--lE2(S0tK}zD&ov-{6bMUc%dNFIn{2^vjXWlt>+uxw#d)T6HNk6MjsfN~4 zDlq#Jjp_!wn}$wfs!f8NX3Rk#9)Q6-jD;D9D=1{$`3?o~caZjXU*U32^JkJ$ZzJ_% zQWNfcImxb!AV1DRBq`-qTV@g1#BT>TlvktYOBviCY!13Bv?_hGYDK}MINVi;pg)V- z($Bx1Tj`c?1I3pYg+i_cvFtcQ$SV9%%9QBPg&8R~Ig$eL+xKZY!C=;M1|r)$&9J2x z;l^a*Ph+isNl*%y1T4SviuK1Nco_spQ25v5-}7u?T9zHB5~{-+W*y3p{yjn{1obqf zYL`J^Uz8zZZN8c4Dxy~)k3Ws)E5eYi+V2C!+7Sm0uu{xq)S8o{9uszFTnE>lPhY=5 zdke-B8_*KwWOd%tQs_zf0x9+YixHp+Qi_V$aYVc$P-1mg?2|_{BUr$6WtLdIX2FaF zGmPRTrdIz)DNE)j*_>b9E}sp*(1-16}u za`dgT`KtA3;+e~9{KV48RT=CGPaVt;>-35}%nlFUMK0y7nOjoYds7&Ft~#>0$^ciZ zM}!J5Mz{&|&lyG^bnmh?YtR z*Z5EfDxkrI{QS#Iq752aiA~V)DRlC*2jlA|nCU!@CJwxO#<=j6ssn;muv zhBT9~35VtwsoSLf*(7vl&{u7d_K_CSBMbzr zzyjt&V5O#8VswCRK3AvVbS7U5(KvTPyUc0BhQ}wy0z3LjcdqH8`6F3!`)b3(mOSxL z>i4f8xor(#V+&#ph~ycJMcj#qeehjxt=~Na>dx#Tcq6Xi4?BnDeu5WBBxt603*BY& zZ#;o1kv?qpZjwK-E{8r4v1@g*lwb|8w@oR3BTDcbiGKs)a>Fpxfzh&b ziQANuJ_tNHdx;a*JeCo^RkGC$(TXS;jnxk=dx++D8|dmPP<0@ z$wh#ZYI%Rx$NKe-)BlJzB*bot0ras3I%`#HTMDthGtM_G6u-(tSroGp1Lz+W1Y`$@ zP`9NK^|IHbBrJ#AL3!X*g3{arc@)nuqa{=*2y+DvSwE=f*{>z1HX(>V zNE$>bbc}_yAu4OVn;8LG^naq5HZY zh{Hec==MD+kJhy6t=Nro&+V)RqORK&ssAxioc7-L#UQuPi#3V2pzfh6Ar400@iuV5 z@r>+{-yOZ%XQhsSfw%;|a4}XHaloW#uGluLKux0II9S1W4w=X9J=(k&8KU()m}b{H zFtoD$u5JlGfpX^&SXHlp$J~wk|DL^YVNh2w(oZ~1*W156YRmenU;g=mI zw({B(QVo2JpJ?pJqu9vijk$Cn+%PSw&b4c@uU6vw)DjGm2WJKt!X}uZ43XYlDIz%& z=~RlgZpU-tu_rD`5!t?289PTyQ zZgAEp=zMK>RW9^~gyc*x%vG;l+c-V?}Bm;^{RpgbEnt_B!FqvnvSy)T=R zGa!5GACDk{9801o@j>L8IbKp#!*Td5@vgFKI4w!5?R{>@^hd8ax{l=vQnd2RDHopo zwA+qb2cu4Rx9^Bu1WNYT`a(g}=&&vT`&Sqn-irxzX_j1=tIE#li`Hn=ht4KQXp zzZj`JO+wojs0dRA#(bXBOFn**o+7rPY{bM9m<+UBF{orv$#yF8)AiOWfuas5Fo`CJ zqa;jAZU^!bh8sjE7fsoPn%Tw11+vufr;NMm3*zC=;jB{R49e~BDeMR+H6MGzDlcA^ zKg>JEL~6_6iaR4i`tSfUhkgPaLXZ<@L7poRF?dw_DzodYG{Gp7#24<}=18PBT}aY` z{)rrt`g}930jr3^RBQNA$j!vzTh#Mo1VL`QCA&US?;<2`P+xy8b9D_Hz>FGHC2r$m zW>S9ywTSdQI5hh%7^e`#r#2906T?))i59O(V^Rpxw42rCAu-+I3y#Pg6cm#&AX%dy ze=hv0cUMxxxh1NQEIYXR{IBM&Bk8FK3NZI3z+M>r@A$ocd*e%x-?W;M0pv50p+MVt zugo<@_ij*6RZ;IPtT_sOf2Zv}-3R_1=sW37GgaF9Ti(>V z1L4ju8RzM%&(B}JpnHSVSs2LH#_&@`4Kg1)>*)^i`9-^JiPE@=4l$+?NbAP?44hX&XAZy&?}1;=8c(e0#-3bltVWg6h=k!(mCx=6DqOJ-I!-(g;*f~DDe={{JGtH7=UY|0F zNk(YyXsGi;g%hB8x)QLpp;;`~4rx>zr3?A|W$>xj>^D~%CyzRctVqtiIz7O3pc@r@JdGJiH@%XR_9vaYoV?J3K1cT%g1xOYqhXfSa`fg=bCLy% zWG74UTdouXiH$?H()lyx6QXt}AS)cOa~3IdBxddcQp;(H-O}btpXR-iwZ5E)di9Jf zfToEu%bOR11xf=Knw7JovRJJ#xZDgAvhBDF<8mDu+Q|!}Z?m_=Oy%Ur4p<71cD@0OGZW+{-1QT?U%_PJJ8T!0d2*a9I2;%|A z9LrfBU!r9qh4=3Mm3nR_~X-EyNc<;?m`?dKUNetCnS)}_-%QcWuOpw zAdZF`4c_24z&m{H9-LIL`=Hrx%{IjrNZ~U<7k6p{_wRkR84g>`eUBOQd3x5 zT^kISYq)gGw?IB8(lu1=$#Vl?iZdrx$H0%NxW)?MO$MhRHn8$F^&mzfMCu>|`{)FL z`ZgOt`z%W~^&kzMAuWy9=q~$ldBftH0}T#(K5e8;j~!x$JjyspJ1IISI?ON5OIPB$ z-5_|YUMb+QUsiv3R%Ys4tVYW+x$}dg;hw%EdoH%SXMp`)v?cxR4wic{X9pVBH>=`#`Kcj!}x4 zV!`6tj|*q?jZdG(CSevn(}4Ogij5 z-kp;sZs}7oNu0x+NHs~(aWaKGV@l~TBkmW&mPj==N!f|1e1SndS6(rPxsn7dz$q_{ zL0jSrihO)1t?gh8N zosMjR3n#YC()CVKv zos2TbnL&)lHEIiYdz|%6N^vAUvTs6?s|~kwI4uXjc9fim`KCqW3D838Xu{48p$2?I zOeEqQe1}JUZECrZSO_m=2<$^rB#B6?nrFXFpi8jw)NmoKV^*Utg6i8aEW|^QNJuW& z4cbXpHSp4|7~TW(%JP%q9W2~@&@5Y5%cXL#fMhV59AGj<3$Hhtfa>24DLk{7GZUtr z5ql**-e58|mbz%5Kk~|f!;g+Ze^b);F+5~^jdoq#m+s?Y*+=d5ruym%-Tnn8htCV; zDyyUrWydgDNM&bI{yp<_wd-q&?Ig+BN-^JjWo6Zu3%Eov^Ja>%eKqrk&7kUqeM8PL zs5D}lTe_Yx;e=K`TDya!-u%y$)r*Cr4bSfN*eZk$XT(Lv2Y}qj&_UaiTevxs_=HXjnOuBpmT> zBg|ty8?|1rD1~Ev^6=C$L9%+RkmBSQxlnj3j$XN?%QBstXdx+Vl!N$f2Ey`i3p@!f zzqhI3jC(TZUx|sP%yValu^nzEV96o%*CljO>I_YKa8wMfc3$_L()k4PB6kglP@IT#wBd*3RITYADL}g+hlzLYxFmCt=_XWS}=jg8`RgJefB57z(2n&&q>m ze&F(YMmoRZW7sQ;cZgd(!A9>7mQ2d#!-?$%G8IQ0`p1|*L&P$GnU0i0^(S;Rua4v8 z_7Qhmv#@+kjS-M|($c*ZOo?V2PgT;GKJyP1REABlZhPyf!kR(0UA7Bww~R<7_u6#t z{XNbiKT&tjne(&=UDZ+gNxf&@9EV|fblS^gxNhI-DH;|`1!YNlMcC{d7I{u_E~cJOalFEzDY|I?S3kHtbrN&}R3k zK(Ph_Ty}*L3Et6$cUW`0}**BY@44KtwEy(jW@pAt`>g> z&8>-TmJiDwc;H%Ae%k6$ndZlfKruu1GocgZrLN=sYI52}_I%d)~ z6z40!%W4I6ch$CE2m>Dl3iwWIbcm27QNY#J!}3hqc&~(F8K{^gIT6E&L!APVaQhj^ zjTJEO&?**pivl^xqfD(rpLu;`Tm1MV+Wtd4u>X6u5V{Yp%)xH$k410o{pGoKdtY0t@GgqFN zO=!hTcYoa^dEPKvPX4ukgUTmR#q840gRMMi%{3kvh9gt(wK;Fniqu9A%BMsq?U&B5DFXC8t8FBN1&UIwS#=S zF(6^Eyn8T}p)4)yRvs2rCXZ{L?N6{hgE_dkH_HA#L3a0$@UMoBw6RE9h|k_rx~%rB zUqeEPL|!Pbp|up2Q=8AcUxflck(fPNJYP1OM_4I(bc24a**Qnd-@;Bkb^2z8Xv?;3yZp*| zoy9KhLo=;8n0rPdQ}yAoS8eb zAtG5QYB|~z@Z(Fxdu`LmoO>f&(JzsO|v0V?1HYsfMvF!3| zka=}6U13(l@$9&=1!CLTCMS~L01CMs@Abl4^Q^YgVgizWaJa%{7t)2sVcZg0mh7>d z(tN=$5$r?s={yA@IX~2ot9`ZGjUgVlul$IU4N}{ zIFBzY3O0;g$BZ#X|VjuTPKyw*|IJ+&pQ` z(NpzU`o=D86kZ3E5#!3Ry$#0AW!6wZe)_xZ8EPidvJ0f+MQJZ6|ZJ$CEV6;Yt{OJnL`dewc1k>AGbkK9Gf5BbB-fg? zgC4#CPYX+9%LLHg@=c;_Vai_~#ksI~)5|9k(W()g6ylc(wP2uSeJ$QLATtq%e#zpT zp^6Y)bV+e_pqIE7#-hURQhfQvIZpMUzD8&-t$esrKJ}4`ZhT|woYi>rP~y~LRf`*2!6 z6prDzJ~1VOlYhYAuBHcu9m>k_F>;N3rpLg>pr;{EDkeQPHfPv~woj$?UTF=txmaZy z?RrVthxVcqUM;X*(=UNg4(L|0d250Xk)6GF&DKD@r6{aZo;(}dnO5@CP7pMmdsI)- zeYH*@#+|)L8x7)@GNBu0Npyyh6r z^~!3$x&w8N)T;|LVgnwx1jHmZn{b2V zO|8s#F0NZhvux?0W9NH5;qZ?P_JtPW86)4J>AS{0F1S0d}=L2`{F z_y;o;17%{j4I)znptnB z%No1W>o}H2%?~CFo~0j?pzWk?dV4ayb!s{#>Yj`ZJ!H)xn}*Z_gFHy~JDis)?9-P=z4iOQg{26~n?dTms7)+F}? zcXvnHHnnbNTzc!$t+V}=<2L<7l(84v1I3b;-)F*Q?cwLNlgg{zi#iS)*rQ5AFWe&~ zWHPPGy{8wEC9JSL?qNVY76=es`bA{vUr~L7f9G@mP}2MNF0Qhv6Sgs`r_k!qRbSXK zv16Qqq`rFM9!4zCrCeiVS~P2e{Pw^A8I?p?NSVR{XfwlQo*wj|Ctqz4X-j+dU7eGkC(2y`(P?FM?P4gKki3Msw#fM6paBq#VNc>T2@``L{DlnnA-_*i10Kre&@-H!Z7gzn9pRF61?^^ z8dJ5kEeVKb%Bly}6NLV}<0(*eZM$QTLcH#+@iWS^>$Of_@Mu1JwM!>&3evymgY6>C_)sK+n|A5G6(3RJz0k>(z2uLdzXeTw)e4*g!h} zn*UvIx-Ozx<3rCF#C`khSv`Y-b&R4gX>d5osr$6jlq^8vi!M$QGx05pJZoY#RGr*J zsJmOhfodAzYQxv-MoU?m_|h^aEwgEHt5h_HMkHwtE+OA03(7{hm1V?AlYAS7G$u5n zO+6?51qo@aQK5#l6pM`kD5OmI28g!J2Z{5kNlSuKl=Yj3QZ|bvVHU}FlM+{QV=<=) z+b|%Q!R)FE z@ycDMSKV2?*XfcAc5@IOrSI&3&aR$|oAD8WNA6O;p~q-J@ll{x`jP<*eEpIYOYnT zer_t=dYw6a0avjQtKN&#n&(KJ5Kr$RXPOp1@Fq#0Of zTXQkq4qQxKWR>x#d{Hyh?6Y)U07;Q$?BTl7mx2bSPY_juXub1 z%-$)NKXzE<%}q>RX25*oeMVjiz&r_z;BrQV-(u>!U>C*OisXNU*UftsrH6vAhTEm@ zoKA`?fZL1sdd!+G@*NNvZa>}37u^x8^T>VH0_6Bx{3@x5NAg&55{2jUE-w3zCJNJi z^IlU=+DJz-9K&4c@7iKj(zlj@%V}27?vYmxo*;!jZVXJMeDg;5T!4Y1rxNV-e$WAu zkk6^Xao8HC=w2hpLvM(!xwo|~$eG6jJj39zyQHf)E+NPJlfspUhzRv&_qr8+Z1`DA zz`EV=A)d=;2&J;eypNx~q&Ir_7e_^xXg(L9>k=X4pxZ3y#-ch$^TN}i>X&uwF%75c(9cjO6`E5 z16vbMYb!lEIM?jxn)^+Ld8*hmEXR4a8TSfqwBg1(@^8$p&#@?iyGd}uhWTVS`Mlpa zGc+kV)K7DJwd46aco@=?iASsx?sDjbHoDVU9=+^tk46|Fxxey1u)_}c1j z^(`5~PU%og1LdSBE5x4N&5&%Nh$sy0oANXwUcGa>@CCMqP`4W$ZPSaykK|giiuMIw zu#j)&VRKWP55I(5K1^cog|iXgaK1Z%wm%T;;M3X`-`TTWaI}NtIZj;CS)S%S(h}qq zRFQ#{m4Qk$7;1i*0PC^|X1@a1pcMq1aiRSCHq+mnfj^FS{oxWs0McCN-lK4>SDp#` z7=Duh)kXC;lr1g3dqogzBBDg6>et<<>m>KO^|bI5X{+eMd^-$2xfoP*&e$vdQc7J% zmFO~OHf7aqlIvg%P`Gu|3n;lKjtRd@;;x#$>_xU(HpZos7?ShZlQSU)bY?qyQM3cHh5twS6^bF8NBKDnJgXHa)? zBYv=GjsZuYC2QFS+jc#uCsaEPEzLSJCL=}SIk9!*2Eo(V*SAUqKw#?um$mUIbqQQb zF1Nn(y?7;gP#@ws$W76>TuGcG=U_f6q2uJq?j#mv7g;llvqu{Yk~Mo>id)jMD7;T> zSB$1!g)QpIf*f}IgmV;!B+3u(ifW%xrD=`RKt*PDC?M5KI)DO`VXw(7X-OMLd3iVU z0CihUN(eNrY;m?vwK{55MU`p1;JDF=6ITN$+!q8W#`iIsN8;W7H?`htf%RS9Lh+KQ z_p_4?qO4#*`t+8l-N|kAKDcOt zoHsqz_oO&n?@4^Mr*4YrkDX44BeS*0zaA1j@*c}{$;jUxRXx1rq7z^*NX6d`DcQ}L z6*cN7e%`2#_J4z8=^GM6>%*i>>X^_0u9qn%0JTUo)c0zIz|7a`%_UnB)-I1cc+ z0}jAK0}jBl|6-2VT759oxBnf%-;7vs>7Mr}0h3^$0`5FAy}2h{ps5%RJA|^~6uCqg zxBMK5bQVD{Aduh1lu4)`Up*&( zCJQ>nafDb#MuhSZ5>YmD@|TcrNv~Q%!tca;tyy8Iy2vu2CeA+AsV^q*Wohg%69XYq zP0ppEDEYJ9>Se&X(v=U#ibxg()m=83pLc*|otbG;`CYZ z*YgsakGO$E$E_$|3bns7`m9ARe%myU3$DE;RoQ<6hR8e;%`pxO1{GXb$cCZl9lVnJ$(c` z``G?|PhXaz`>)rb7jm2#v7=(W?@ zjUhrNndRFMQ}%^^(-nmD&J>}9w@)>l;mhRr@$}|4ueOd?U9ZfO-oi%^n4{#V`i}#f zqh<@f^%~(MnS?Z0xsQI|Fghrby<&{FA+e4a>c(yxFL!Pi#?DW!!YI{OmR{xEC7T7k zS_g*9VWI}d0IvIXx*d5<7$5Vs=2^=ews4qZGmAVyC^9e;wxJ%BmB(F5*&!yyABCtLVGL@`qW>X9K zpv=W~+EszGef=am3LG+#yIq5oLXMnZ_dxSLQ_&bwjC^0e8qN@v!p?7mg02H<9`uaJ zy0GKA&YQV2CxynI3T&J*m!rf4@J*eo235*!cB1zEMQZ%h5>GBF;8r37K0h?@|E*0A zIHUg0y7zm(rFKvJS48W7RJwl!i~<6X2Zw+Fbm9ekev0M;#MS=Y5P(kq^(#q11zsvq zDIppe@xOMnsOIK+5BTFB=cWLalK#{3eE>&7fd11>l2=MpNKjsZT2kmG!jCQh`~Fu0 z9P0ab`$3!r`1yz8>_7DYsO|h$kIsMh__s*^KXv?Z1O8|~sEz?Y{+GDzze^GPjk$E$ zXbA-1gd77#=tn)YKU=;JE?}De0)WrT%H9s3`fn|%YibEdyZov3|MJ>QWS>290eCZj z58i<*>dC9=kz?s$sP_9kK1p>nV3qvbleExyq56|o+oQsb{ZVmuu1n~JG z0sUvo_i4fSM>xRs8rvG$*+~GZof}&ISxn(2JU*K{L<3+b{bBw{68H&Uiup@;fWWl5 zgB?IWMab0LkXK(Hz#yq>scZbd2%=B?DO~^q9tarlzZysN+g}n0+v);JhbjUT8AYrt z3?;0r%p9zLJv1r$%q&HKF@;3~0wVwO!U5m;J`Mm|`Nc^80sZd+Wj}21*SPoF82hCF zoK?Vw;4ioafdAkZxT1er-LLVi-*0`@2Ur&*!b?0U>R;no+S%)xoBuBxRw$?weN-u~tKE}8xb@7Gs%(aC;e1-LIlSfXDK(faFW)mnHdrLc3`F z6ZBsT^u0uVS&il=>YVX^*5`k!P4g1)2LQmz{?&dgf`7JrA4ZeE0sikL`k!Eb6r=g0 z{aCy_0I>fxSAXQYz3lw5G|ivg^L@(x-uch!AphH+d;E4`175`R0#b^)Zp>EM1Ks=zx6_261>!7 z{7F#a{Tl@Tpw9S`>7_i|PbScS-(dPJv9_0-FBP_aa@Gg^2IoKNZM~#=sW$SH3MJ|{ zsQy8F43lX7hYx<{v^Q9`2QsMzeen3cGpiTgzVp- z`aj3&Wv0(he1qKI!2jpGpO-i0Wpcz%vdn`2o9x&3;^nsZPt3c \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + command -v java >/dev/null || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/icons/aux1_btn.svg b/android/icons/aux1_btn.svg new file mode 100644 index 0000000..e0ee97c --- /dev/null +++ b/android/icons/aux1_btn.svg @@ -0,0 +1,143 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + Aux1 + + + diff --git a/android/icons/camera_btn.svg b/android/icons/camera_btn.svg new file mode 100644 index 0000000..a91a7fc --- /dev/null +++ b/android/icons/camera_btn.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/chat_btn.svg b/android/icons/chat_btn.svg new file mode 100644 index 0000000..41dc6f8 --- /dev/null +++ b/android/icons/chat_btn.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/android/icons/chat_hide_btn.svg b/android/icons/chat_hide_btn.svg new file mode 100644 index 0000000..6647b30 --- /dev/null +++ b/android/icons/chat_hide_btn.svg @@ -0,0 +1,139 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/chat_show_btn.svg b/android/icons/chat_show_btn.svg new file mode 100644 index 0000000..fce9de9 --- /dev/null +++ b/android/icons/chat_show_btn.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/checkbox_tick.svg b/android/icons/checkbox_tick.svg new file mode 100644 index 0000000..6b727bb --- /dev/null +++ b/android/icons/checkbox_tick.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/android/icons/debug_btn.svg b/android/icons/debug_btn.svg new file mode 100644 index 0000000..2c37f14 --- /dev/null +++ b/android/icons/debug_btn.svg @@ -0,0 +1,344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/down.svg b/android/icons/down.svg new file mode 100644 index 0000000..190e7e8 --- /dev/null +++ b/android/icons/down.svg @@ -0,0 +1,542 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/drop_btn.svg b/android/icons/drop_btn.svg new file mode 100644 index 0000000..7cb0e85 --- /dev/null +++ b/android/icons/drop_btn.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/fast_btn.svg b/android/icons/fast_btn.svg new file mode 100644 index 0000000..1436596 --- /dev/null +++ b/android/icons/fast_btn.svg @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/fly_btn.svg b/android/icons/fly_btn.svg new file mode 100644 index 0000000..d203842 --- /dev/null +++ b/android/icons/fly_btn.svg @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/gear_icon.svg b/android/icons/gear_icon.svg new file mode 100644 index 0000000..b44685a --- /dev/null +++ b/android/icons/gear_icon.svg @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/inventory_btn.svg b/android/icons/inventory_btn.svg new file mode 100644 index 0000000..ee3dc3c --- /dev/null +++ b/android/icons/inventory_btn.svg @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/joystick_bg.svg b/android/icons/joystick_bg.svg new file mode 100644 index 0000000..d8836b3 --- /dev/null +++ b/android/icons/joystick_bg.svg @@ -0,0 +1,876 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/joystick_center.svg b/android/icons/joystick_center.svg new file mode 100644 index 0000000..1720229 --- /dev/null +++ b/android/icons/joystick_center.svg @@ -0,0 +1,877 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/joystick_off.svg b/android/icons/joystick_off.svg new file mode 100644 index 0000000..58e1acf --- /dev/null +++ b/android/icons/joystick_off.svg @@ -0,0 +1,882 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/jump_btn.svg b/android/icons/jump_btn.svg new file mode 100644 index 0000000..882c49e --- /dev/null +++ b/android/icons/jump_btn.svg @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/minimap_btn.svg b/android/icons/minimap_btn.svg new file mode 100644 index 0000000..deda327 --- /dev/null +++ b/android/icons/minimap_btn.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/noclip_btn.svg b/android/icons/noclip_btn.svg new file mode 100644 index 0000000..a816edf --- /dev/null +++ b/android/icons/noclip_btn.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/rangeview_btn.svg b/android/icons/rangeview_btn.svg new file mode 100644 index 0000000..f9319e0 --- /dev/null +++ b/android/icons/rangeview_btn.svg @@ -0,0 +1,456 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/rare_controls.svg b/android/icons/rare_controls.svg new file mode 100644 index 0000000..c9991ec --- /dev/null +++ b/android/icons/rare_controls.svg @@ -0,0 +1,521 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/icons/zoom.svg b/android/icons/zoom.svg new file mode 100644 index 0000000..ea8dec3 --- /dev/null +++ b/android/icons/zoom.svg @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/keystore-minetest.jks b/android/keystore-minetest.jks new file mode 100644 index 0000000000000000000000000000000000000000..8fce68bbbd23f912b69a4f94303c396f36444f63 GIT binary patch literal 2247 zcmchY={wX58^`B2i)Aom$`;wekmWZTJ4Ir$Wyw0HED4E58GDgNAt{9-lx1RM#L#e3 z)}++fmy>m}J2;qzh#uq7xz2N4=P!6(+%LYL`^|lQ@B8}R+-2@E1OkEX74UcQT@EDr zh53=f_JFNqfB_Q%K>#2bz5}9okVkj`IG_gI2LK2NoD5%BQ^G&%@9LOWM1Rm*r$01A zr?CosQhTntWR+Ul40}hkLXni2lW>hN#j-~a-Q2WTz9(-zM+B;v@UI+ZK~k13EjNCh z@8CG;F=azAQ-WF#PG^+xYIjIbM?A3JN>=Y(nX64Vp;QBRlO((AF>aX$LJZQ1Y{R?p zjQf?m_%-`}LC`FY`dy=c#TQ4tJhsLDSK6#290o!7&$skt4k=mY27GYamt1h z#X8I7uFS@5m&i6w_^J*O379?^nRwgGW+QaX!n$bp&kAKn#_!?QQE9x#e6IG$8Qd2j zmtJX(F~>(KmE%6&Q~5rQ*Pmw3ZHz%G*K9@7^GruRMLrPing0vF8K1jR^+GRZ`fkjj z{lJQ8&?*%*`6;KWdjW~@goz|@It#K6>Kb@ruH--m%*PKO4&jt|T_h!gwigl$bR^X_dE3;dpNX zch%BndsCkpI>nIRO2Y22Dw>TOB+HWmAXN(<2(jq-;Pt4^(p9Az_MetEb3W5iExP3) z7sVNb5y#>w3C#&rt(T$2V@Hu1S;&Z>MdRsU!S>@Y7v+V^Idtn6l8ib#F9wzoHfe)! zg#|smF@KYI_hZGKVPJqHMSeSbIjPC%i*&JsgVokUtHD_bm)rqz!|SB&P=V?SQ%LKXBR2&bU#3lRvT&lOy}X;}?oOQ;?Apc<+uj%~@L z-3jKF$k3Irw*^5)sow>6s`4tW0_WQkP3 zw(i}wx9BnUY^m!T$q3Gy*IzEy9>)PJgz0Dg$Eu#b`}5=S#+vMlJl8r1jAL#G5m^E= zpGAv|63-FOSi$%AIk))W9w5c*TP4g)FDc4ODA%%0R3QkXHD`s@Dvq=~@ z))uiSS71MH(5cp^c`9> zD<0po7bjm9n6H8B=+)8LDGvq0M^%K-Rwd+B;>gq<<*7_KWt~a zY+K-D{U0RM4#vDK=k&e4WM_*aLEU-u)m`aK9!X7jm%vSCK97FAu+X`J+n zAYlbTa1CX{yIWvS8WT9GS}n0U%GmRYd+@b zsy`c`FaQEjaUSJKX z@@M|Qkfh7`vo~W4w$jaJ8%RT?l%F;4H&#_f5+_wXLe7lK3Q%a8J)^y)F#|!I^TB7A zGvudRO_LAS8b8*wppMU75V%(PA=TKlfRIT{mYOpvl8c_?^wS0klMQc?!`c~Ze~1b3 R1A6h&3a;N$_-18${|h>+-YozC literal 0 HcmV?d00001 diff --git a/android/native/build.gradle b/android/native/build.gradle new file mode 100644 index 0000000..90e4fe2 --- /dev/null +++ b/android/native/build.gradle @@ -0,0 +1,69 @@ +apply plugin: 'com.android.library' +apply plugin: 'de.undercouch.download' + +android { + compileSdkVersion 30 + buildToolsVersion '30.0.3' + ndkVersion "$ndk_version" + defaultConfig { + minSdkVersion 16 + targetSdkVersion 30 + externalNativeBuild { + ndkBuild { + arguments '-j' + Runtime.getRuntime().availableProcessors(), + "versionMajor=${versionMajor}", + "versionMinor=${versionMinor}", + "versionPatch=${versionPatch}", + "versionExtra=${versionExtra}", + "developmentBuild=${developmentBuild}" + } + } + } + + externalNativeBuild { + ndkBuild { + path file('jni/Android.mk') + } + } + + // supported architectures + splits { + abi { + enable true + reset() + include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' + } + } + + buildTypes { + release { + externalNativeBuild { + ndkBuild { + arguments 'NDEBUG=1' + } + } + + ndk { + debugSymbolLevel 'SYMBOL_TABLE' + } + } + } +} + +// get precompiled deps +task downloadDeps(type: Download) { + src 'https://github.com/minetest/minetest_android_deps/releases/download/latest/deps.zip' + dest new File(buildDir, 'deps.zip') + overwrite false +} + +task getDeps(dependsOn: downloadDeps, type: Copy) { + def deps = new File(buildDir.parent, 'deps') + if (!deps.exists()) { + deps.mkdir() + from zipTree(downloadDeps.dest) + into deps + } +} + +preBuild.dependsOn getDeps diff --git a/android/native/jni/Android.mk b/android/native/jni/Android.mk new file mode 100644 index 0000000..cd9326d --- /dev/null +++ b/android/native/jni/Android.mk @@ -0,0 +1,301 @@ +LOCAL_PATH := $(call my-dir)/.. + +#LOCAL_ADDRESS_SANITIZER:=true +#USE_BUILTIN_LUA:=true + +include $(CLEAR_VARS) +LOCAL_MODULE := Curl +LOCAL_SRC_FILES := deps/$(APP_ABI)/Curl/libcurl.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libmbedcrypto +LOCAL_SRC_FILES := deps/$(APP_ABI)/Curl/libmbedcrypto.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libmbedtls +LOCAL_SRC_FILES := deps/$(APP_ABI)/Curl/libmbedtls.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libmbedx509 +LOCAL_SRC_FILES := deps/$(APP_ABI)/Curl/libmbedx509.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := Freetype +LOCAL_SRC_FILES := deps/$(APP_ABI)/Freetype/libfreetype.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := Iconv +LOCAL_SRC_FILES := deps/$(APP_ABI)/Iconv/libiconv.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libcharset +LOCAL_SRC_FILES := deps/$(APP_ABI)/Iconv/libcharset.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := Irrlicht +LOCAL_SRC_FILES := deps/$(APP_ABI)/Irrlicht/libIrrlichtMt.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := Irrlicht-libpng +LOCAL_SRC_FILES := deps/$(APP_ABI)/Irrlicht/libpng.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := Irrlicht-libjpeg +LOCAL_SRC_FILES := deps/$(APP_ABI)/Irrlicht/libjpeg.a +include $(PREBUILT_STATIC_LIBRARY) + +ifndef USE_BUILTIN_LUA + +include $(CLEAR_VARS) +LOCAL_MODULE := LuaJIT +LOCAL_SRC_FILES := deps/$(APP_ABI)/LuaJIT/libluajit.a +include $(PREBUILT_STATIC_LIBRARY) + +endif + +include $(CLEAR_VARS) +LOCAL_MODULE := OpenAL +LOCAL_SRC_FILES := deps/$(APP_ABI)/OpenAL-Soft/libopenal.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := Gettext +LOCAL_SRC_FILES := deps/$(APP_ABI)/Gettext/libintl.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := SQLite3 +LOCAL_SRC_FILES := deps/$(APP_ABI)/SQLite/libsqlite3.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := Vorbis +LOCAL_SRC_FILES := deps/$(APP_ABI)/Vorbis/libvorbis.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libvorbisfile +LOCAL_SRC_FILES := deps/$(APP_ABI)/Vorbis/libvorbisfile.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := libogg +LOCAL_SRC_FILES := deps/$(APP_ABI)/Vorbis/libogg.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := Zstd +LOCAL_SRC_FILES := deps/$(APP_ABI)/Zstd/libzstd.a +include $(PREBUILT_STATIC_LIBRARY) + +include $(CLEAR_VARS) +LOCAL_MODULE := Minetest + +LOCAL_CFLAGS += \ + -DJSONCPP_NO_LOCALE_SUPPORT \ + -DHAVE_TOUCHSCREENGUI \ + -DENABLE_GLES=1 \ + -DUSE_CURL=1 \ + -DUSE_SOUND=1 \ + -DUSE_LEVELDB=0 \ + -DUSE_GETTEXT=1 \ + -DVERSION_MAJOR=${versionMajor} \ + -DVERSION_MINOR=${versionMinor} \ + -DVERSION_PATCH=${versionPatch} \ + -DVERSION_EXTRA=${versionExtra} \ + -DDEVELOPMENT_BUILD=${developmentBuild} \ + $(GPROF_DEF) + +ifdef USE_BUILTIN_LUA + LOCAL_CFLAGS += -DUSE_LUAJIT=0 +else + LOCAL_CFLAGS += -DUSE_LUAJIT=1 +endif + +ifdef NDEBUG + LOCAL_CFLAGS += -DNDEBUG=1 +endif + +ifdef GPROF + GPROF_DEF := -DGPROF + PROFILER_LIBS := android-ndk-profiler + LOCAL_CFLAGS += -pg +endif + +LOCAL_C_INCLUDES := \ + ../../src \ + ../../src/script \ + ../../lib/gmp \ + ../../lib/jsoncpp \ + deps/$(APP_ABI)/Curl/include \ + deps/$(APP_ABI)/Freetype/include/freetype2 \ + deps/$(APP_ABI)/Irrlicht/include \ + deps/$(APP_ABI)/Gettext/include \ + deps/$(APP_ABI)/Iconv/include \ + deps/$(APP_ABI)/OpenAL-Soft/include \ + deps/$(APP_ABI)/SQLite/include \ + deps/$(APP_ABI)/Vorbis/include \ + deps/$(APP_ABI)/Zstd/include + +ifdef USE_BUILTIN_LUA + LOCAL_C_INCLUDES += \ + ../../lib/lua/src \ + ../../lib/bitop +else + LOCAL_C_INCLUDES += deps/$(APP_ABI)/LuaJIT/include +endif + +LOCAL_SRC_FILES := \ + $(wildcard ../../src/client/*.cpp) \ + $(wildcard ../../src/client/*/*.cpp) \ + $(wildcard ../../src/content/*.cpp) \ + ../../src/database/database.cpp \ + ../../src/database/database-dummy.cpp \ + ../../src/database/database-files.cpp \ + ../../src/database/database-sqlite3.cpp \ + $(wildcard ../../src/gui/*.cpp) \ + $(wildcard ../../src/irrlicht_changes/*.cpp) \ + $(wildcard ../../src/mapgen/*.cpp) \ + $(wildcard ../../src/network/*.cpp) \ + $(wildcard ../../src/script/*.cpp) \ + $(wildcard ../../src/script/*/*.cpp) \ + $(wildcard ../../src/server/*.cpp) \ + $(wildcard ../../src/threading/*.cpp) \ + $(wildcard ../../src/util/*.c) \ + $(wildcard ../../src/util/*.cpp) \ + ../../src/ban.cpp \ + ../../src/chat.cpp \ + ../../src/clientiface.cpp \ + ../../src/collision.cpp \ + ../../src/content_mapnode.cpp \ + ../../src/content_nodemeta.cpp \ + ../../src/convert_json.cpp \ + ../../src/craftdef.cpp \ + ../../src/debug.cpp \ + ../../src/defaultsettings.cpp \ + ../../src/emerge.cpp \ + ../../src/environment.cpp \ + ../../src/face_position_cache.cpp \ + ../../src/filesys.cpp \ + ../../src/gettext.cpp \ + ../../src/httpfetch.cpp \ + ../../src/hud.cpp \ + ../../src/inventory.cpp \ + ../../src/inventorymanager.cpp \ + ../../src/itemdef.cpp \ + ../../src/itemstackmetadata.cpp \ + ../../src/light.cpp \ + ../../src/log.cpp \ + ../../src/main.cpp \ + ../../src/map.cpp \ + ../../src/map_settings_manager.cpp \ + ../../src/mapblock.cpp \ + ../../src/mapnode.cpp \ + ../../src/mapsector.cpp \ + ../../src/metadata.cpp \ + ../../src/modchannels.cpp \ + ../../src/nameidmapping.cpp \ + ../../src/nodedef.cpp \ + ../../src/nodemetadata.cpp \ + ../../src/nodetimer.cpp \ + ../../src/noise.cpp \ + ../../src/objdef.cpp \ + ../../src/object_properties.cpp \ + ../../src/particles.cpp \ + ../../src/pathfinder.cpp \ + ../../src/player.cpp \ + ../../src/porting.cpp \ + ../../src/porting_android.cpp \ + ../../src/profiler.cpp \ + ../../src/raycast.cpp \ + ../../src/reflowscan.cpp \ + ../../src/remoteplayer.cpp \ + ../../src/rollback.cpp \ + ../../src/rollback_interface.cpp \ + ../../src/serialization.cpp \ + ../../src/server.cpp \ + ../../src/serverenvironment.cpp \ + ../../src/serverlist.cpp \ + ../../src/settings.cpp \ + ../../src/staticobject.cpp \ + ../../src/texture_override.cpp \ + ../../src/tileanimation.cpp \ + ../../src/tool.cpp \ + ../../src/translation.cpp \ + ../../src/version.cpp \ + ../../src/voxel.cpp \ + ../../src/voxelalgorithms.cpp + +# Built-in Lua +ifdef USE_BUILTIN_LUA + LOCAL_SRC_FILES += \ + ../../lib/lua/src/lapi.c \ + ../../lib/lua/src/lauxlib.c \ + ../../lib/lua/src/lbaselib.c \ + ../../lib/lua/src/lcode.c \ + ../../lib/lua/src/ldblib.c \ + ../../lib/lua/src/ldebug.c \ + ../../lib/lua/src/ldo.c \ + ../../lib/lua/src/ldump.c \ + ../../lib/lua/src/lfunc.c \ + ../../lib/lua/src/lgc.c \ + ../../lib/lua/src/linit.c \ + ../../lib/lua/src/liolib.c \ + ../../lib/lua/src/llex.c \ + ../../lib/lua/src/lmathlib.c \ + ../../lib/lua/src/lmem.c \ + ../../lib/lua/src/loadlib.c \ + ../../lib/lua/src/lobject.c \ + ../../lib/lua/src/lopcodes.c \ + ../../lib/lua/src/loslib.c \ + ../../lib/lua/src/lparser.c \ + ../../lib/lua/src/lstate.c \ + ../../lib/lua/src/lstring.c \ + ../../lib/lua/src/lstrlib.c \ + ../../lib/lua/src/ltable.c \ + ../../lib/lua/src/ltablib.c \ + ../../lib/lua/src/ltm.c \ + ../../lib/lua/src/lundump.c \ + ../../lib/lua/src/lvm.c \ + ../../lib/lua/src/lzio.c \ + ../../lib/bitop/bit.c +endif + +# GMP +LOCAL_SRC_FILES += ../../lib/gmp/mini-gmp.c + +# JSONCPP +LOCAL_SRC_FILES += ../../lib/jsoncpp/jsoncpp.cpp + +LOCAL_STATIC_LIBRARIES += \ + Curl libmbedcrypto libmbedtls libmbedx509 \ + Freetype \ + Iconv libcharset \ + Irrlicht Irrlicht-libpng Irrlicht-libjpeg \ + OpenAL \ + Gettext \ + SQLite3 \ + Vorbis libvorbisfile libogg \ + Zstd +ifndef USE_BUILTIN_LUA + LOCAL_STATIC_LIBRARIES += LuaJIT +endif +LOCAL_STATIC_LIBRARIES += android_native_app_glue $(PROFILER_LIBS) + +LOCAL_LDLIBS := -lEGL -lGLESv1_CM -lGLESv2 -landroid -lOpenSLES -lz + +include $(BUILD_SHARED_LIBRARY) + +ifdef GPROF +$(call import-module,android-ndk-profiler) +endif +$(call import-module,android/native_app_glue) diff --git a/android/native/jni/Application.mk b/android/native/jni/Application.mk new file mode 100644 index 0000000..9d95961 --- /dev/null +++ b/android/native/jni/Application.mk @@ -0,0 +1,32 @@ +APP_PLATFORM := ${APP_PLATFORM} +APP_ABI := ${TARGET_ABI} +APP_STL := c++_shared +NDK_TOOLCHAIN_VERSION := clang +APP_SHORT_COMMANDS := true +APP_MODULES := Minetest + +APP_CPPFLAGS := -O2 -fvisibility=hidden + +ifeq ($(APP_ABI),armeabi-v7a) +APP_CPPFLAGS += -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb +endif + +ifeq ($(APP_ABI),x86) +APP_CPPFLAGS += -mssse3 -mfpmath=sse -funroll-loops +endif + +ifndef NDEBUG +APP_CPPFLAGS := -g -Og -fno-omit-frame-pointer +endif + +APP_CFLAGS := $(APP_CPPFLAGS) -Wno-inconsistent-missing-override -Wno-parentheses-equality +APP_CXXFLAGS := $(APP_CPPFLAGS) -fexceptions -frtti -std=gnu++14 +APP_LDFLAGS := -Wl,--no-warn-mismatch,--gc-sections,--icf=safe + +ifeq ($(APP_ABI),arm64-v8a) +APP_LDFLAGS := -Wl,--no-warn-mismatch,--gc-sections +endif + +ifndef NDEBUG +APP_LDFLAGS := +endif diff --git a/android/native/src/main/AndroidManifest.xml b/android/native/src/main/AndroidManifest.xml new file mode 100644 index 0000000..19451c7 --- /dev/null +++ b/android/native/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..b048fca --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = "Minetest" +include ':app', ':native' diff --git a/builtin/async/game.lua b/builtin/async/game.lua new file mode 100644 index 0000000..6512f07 --- /dev/null +++ b/builtin/async/game.lua @@ -0,0 +1,59 @@ +core.log("info", "Initializing asynchronous environment (game)") + +local function pack2(...) + return {n=select('#', ...), ...} +end + +-- Entrypoint to run async jobs, called by C++ +function core.job_processor(func, params) + local retval = pack2(func(unpack(params, 1, params.n))) + + return retval +end + +-- Import a bunch of individual files from builtin/game/ +local gamepath = core.get_builtin_path() .. "game" .. DIR_DELIM + +dofile(gamepath .. "constants.lua") +dofile(gamepath .. "item_s.lua") +dofile(gamepath .. "misc_s.lua") +dofile(gamepath .. "features.lua") +dofile(gamepath .. "voxelarea.lua") + +-- Transfer of globals +do + local all = assert(core.transferred_globals) + core.transferred_globals = nil + + all.registered_nodes = {} + all.registered_craftitems = {} + all.registered_tools = {} + for k, v in pairs(all.registered_items) do + -- Disable further modification + setmetatable(v, {__newindex = {}}) + -- Reassemble the other tables + if v.type == "node" then + all.registered_nodes[k] = v + elseif v.type == "craftitem" then + all.registered_craftitems[k] = v + elseif v.type == "tool" then + all.registered_tools[k] = v + end + end + + for k, v in pairs(all) do + core[k] = v + end +end + +-- For tables that are indexed by item name: +-- If table[X] does not exist, default to table[core.registered_aliases[X]] +local alias_metatable = { + __index = function(t, name) + return rawget(t, core.registered_aliases[name]) + end +} +setmetatable(core.registered_items, alias_metatable) +setmetatable(core.registered_nodes, alias_metatable) +setmetatable(core.registered_craftitems, alias_metatable) +setmetatable(core.registered_tools, alias_metatable) diff --git a/builtin/async/mainmenu.lua b/builtin/async/mainmenu.lua new file mode 100644 index 0000000..0e9c222 --- /dev/null +++ b/builtin/async/mainmenu.lua @@ -0,0 +1,9 @@ +core.log("info", "Initializing asynchronous environment") + +function core.job_processor(func, serialized_param) + local param = core.deserialize(serialized_param) + + local retval = core.serialize(func(param)) + + return retval or core.serialize(nil) +end diff --git a/builtin/client/chatcommands.lua b/builtin/client/chatcommands.lua new file mode 100644 index 0000000..a563a66 --- /dev/null +++ b/builtin/client/chatcommands.lua @@ -0,0 +1,74 @@ +-- Minetest: builtin/client/chatcommands.lua + +core.register_on_sending_chat_message(function(message) + if message:sub(1,2) == ".." then + return false + end + + local first_char = message:sub(1,1) + if first_char == "/" or first_char == "." then + core.display_chat_message(core.gettext("Issued command: ") .. message) + end + + if first_char ~= "." then + return false + end + + local cmd, param = string.match(message, "^%.([^ ]+) *(.*)") + param = param or "" + + if not cmd then + core.display_chat_message("-!- " .. core.gettext("Empty command.")) + return true + end + + -- Run core.registered_on_chatcommand callbacks. + if core.run_callbacks(core.registered_on_chatcommand, 5, cmd, param) then + return true + end + + local cmd_def = core.registered_chatcommands[cmd] + if cmd_def then + core.set_last_run_mod(cmd_def.mod_origin) + local _, result = cmd_def.func(param) + if result then + core.display_chat_message(result) + end + else + core.display_chat_message("-!- " .. core.gettext("Invalid command: ") .. cmd) + end + + return true +end) + +core.register_chatcommand("list_players", { + description = core.gettext("List online players"), + func = function(param) + local player_names = core.get_player_names() + if not player_names then + return false, core.gettext("This command is disabled by server.") + end + + local players = table.concat(player_names, ", ") + return true, core.gettext("Online players: ") .. players + end +}) + +core.register_chatcommand("disconnect", { + description = core.gettext("Exit to main menu"), + func = function(param) + core.disconnect() + end, +}) + +core.register_chatcommand("clear_chat_queue", { + description = core.gettext("Clear the out chat queue"), + func = function(param) + core.clear_out_chat_queue() + return true, core.gettext("The out chat queue is now empty.") + end, +}) + +function core.run_server_chatcommand(cmd, param) + core.send_chat_message("/" .. cmd .. " " .. param) +end diff --git a/builtin/client/death_formspec.lua b/builtin/client/death_formspec.lua new file mode 100644 index 0000000..c25c799 --- /dev/null +++ b/builtin/client/death_formspec.lua @@ -0,0 +1,15 @@ +-- CSM death formspec. Only used when clientside modding is enabled, otherwise +-- handled by the engine. + +core.register_on_death(function() + local formspec = "size[11,5.5]bgcolor[#320000b4;true]" .. + "label[4.85,1.35;" .. fgettext("You died") .. + "]button_exit[4,3;3,0.5;btn_respawn;".. fgettext("Respawn") .."]" + core.show_formspec("bultin:death", formspec) +end) + +core.register_on_formspec_input(function(formname, fields) + if formname == "bultin:death" then + core.send_respawn() + end +end) diff --git a/builtin/client/init.lua b/builtin/client/init.lua new file mode 100644 index 0000000..3719a90 --- /dev/null +++ b/builtin/client/init.lua @@ -0,0 +1,12 @@ +-- Minetest: builtin/client/init.lua +local scriptpath = core.get_builtin_path() +local clientpath = scriptpath.."client"..DIR_DELIM +local commonpath = scriptpath.."common"..DIR_DELIM + +dofile(clientpath .. "register.lua") +dofile(commonpath .. "after.lua") +dofile(commonpath .. "mod_storage.lua") +dofile(commonpath .. "chatcommands.lua") +dofile(clientpath .. "chatcommands.lua") +dofile(clientpath .. "death_formspec.lua") +dofile(clientpath .. "misc.lua") diff --git a/builtin/client/misc.lua b/builtin/client/misc.lua new file mode 100644 index 0000000..80e0f29 --- /dev/null +++ b/builtin/client/misc.lua @@ -0,0 +1,7 @@ +function core.setting_get_pos(name) + local value = core.settings:get(name) + if not value then + return nil + end + return core.string_to_pos(value) +end diff --git a/builtin/client/register.lua b/builtin/client/register.lua new file mode 100644 index 0000000..61db4a3 --- /dev/null +++ b/builtin/client/register.lua @@ -0,0 +1,83 @@ +core.callback_origins = {} + +local getinfo = debug.getinfo +debug.getinfo = nil + +--- Runs given callbacks. +-- +-- Note: this function is also called from C++ +-- @tparam table callbacks a table with registered callbacks, like `core.registered_on_*` +-- @tparam number mode a RunCallbacksMode, as defined in src/script/common/c_internal.h +-- @param ... arguments for the callback +-- @return depends on mode +function core.run_callbacks(callbacks, mode, ...) + assert(type(callbacks) == "table") + local cb_len = #callbacks + if cb_len == 0 then + if mode == 2 or mode == 3 then + return true + elseif mode == 4 or mode == 5 then + return false + end + end + local ret + for i = 1, cb_len do + local cb_ret = callbacks[i](...) + + if mode == 0 and i == 1 or mode == 1 and i == cb_len then + ret = cb_ret + elseif mode == 2 then + if not cb_ret or i == 1 then + ret = cb_ret + end + elseif mode == 3 then + if cb_ret then + return cb_ret + end + ret = cb_ret + elseif mode == 4 then + if (cb_ret and not ret) or i == 1 then + ret = cb_ret + end + elseif mode == 5 and cb_ret then + return cb_ret + end + end + return ret +end + +-- +-- Callback registration +-- + +local function make_registration() + local t = {} + local registerfunc = function(func) + t[#t + 1] = func + core.callback_origins[func] = { + mod = core.get_current_modname() or "??", + name = getinfo(1, "n").name or "??" + } + --local origin = core.callback_origins[func] + --print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func)) + end + return t, registerfunc +end + +core.registered_globalsteps, core.register_globalstep = make_registration() +core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration() +core.registered_on_shutdown, core.register_on_shutdown = make_registration() +core.registered_on_receiving_chat_message, core.register_on_receiving_chat_message = make_registration() +core.registered_on_sending_chat_message, core.register_on_sending_chat_message = make_registration() +core.registered_on_chatcommand, core.register_on_chatcommand = make_registration() +core.registered_on_death, core.register_on_death = make_registration() +core.registered_on_hp_modification, core.register_on_hp_modification = make_registration() +core.registered_on_damage_taken, core.register_on_damage_taken = make_registration() +core.registered_on_formspec_input, core.register_on_formspec_input = make_registration() +core.registered_on_dignode, core.register_on_dignode = make_registration() +core.registered_on_punchnode, core.register_on_punchnode = make_registration() +core.registered_on_placenode, core.register_on_placenode = make_registration() +core.registered_on_item_use, core.register_on_item_use = make_registration() +core.registered_on_modchannel_message, core.register_on_modchannel_message = make_registration() +core.registered_on_modchannel_signal, core.register_on_modchannel_signal = make_registration() +core.registered_on_inventory_open, core.register_on_inventory_open = make_registration() diff --git a/builtin/common/after.lua b/builtin/common/after.lua new file mode 100644 index 0000000..bce2625 --- /dev/null +++ b/builtin/common/after.lua @@ -0,0 +1,50 @@ +local jobs = {} +local time = 0.0 +local time_next = math.huge + +core.register_globalstep(function(dtime) + time = time + dtime + + if time < time_next then + return + end + + time_next = math.huge + + -- Iterate backwards so that we miss any new timers added by + -- a timer callback. + for i = #jobs, 1, -1 do + local job = jobs[i] + if time >= job.expire then + core.set_last_run_mod(job.mod_origin) + job.func(unpack(job.arg)) + local jobs_l = #jobs + jobs[i] = jobs[jobs_l] + jobs[jobs_l] = nil + elseif job.expire < time_next then + time_next = job.expire + end + end +end) + +function core.after(after, func, ...) + assert(tonumber(after) and type(func) == "function", + "Invalid minetest.after invocation") + local expire = time + after + local new_job = { + func = func, + expire = expire, + arg = {...}, + mod_origin = core.get_last_run_mod(), + } + + jobs[#jobs + 1] = new_job + time_next = math.min(time_next, expire) + + return { + cancel = function() + new_job.func = function() end + new_job.args = {} + end + } +end diff --git a/builtin/common/chatcommands.lua b/builtin/common/chatcommands.lua new file mode 100644 index 0000000..7c3da06 --- /dev/null +++ b/builtin/common/chatcommands.lua @@ -0,0 +1,178 @@ +-- Minetest: builtin/common/chatcommands.lua + +-- For server-side translations (if INIT == "game") +-- Otherwise, use core.gettext +local S = core.get_translator("__builtin") + +core.registered_chatcommands = {} + +-- Interpret the parameters of a command, separating options and arguments. +-- Input: command, param +-- command: name of command +-- param: parameters of command +-- Returns: opts, args +-- opts is a string of option letters, or false on error +-- args is an array with the non-option arguments in order, or an error message +-- Example: for this command line: +-- /command a b -cd e f -g +-- the function would receive: +-- a b -cd e f -g +-- and it would return: +-- "cdg", {"a", "b", "e", "f"} +-- Negative numbers are taken as arguments. Long options (--option) are +-- currently rejected as reserved. +local function getopts(command, param) + local opts = "" + local args = {} + for match in param:gmatch("%S+") do + if match:byte(1) == 45 then -- 45 = '-' + local second = match:byte(2) + if second == 45 then + return false, S("Invalid parameters (see /help @1).", command) + elseif second and (second < 48 or second > 57) then -- 48 = '0', 57 = '9' + opts = opts .. match:sub(2) + else + -- numeric, add it to args + args[#args + 1] = match + end + else + args[#args + 1] = match + end + end + return opts, args +end + +function core.register_chatcommand(cmd, def) + def = def or {} + def.params = def.params or "" + def.description = def.description or "" + def.privs = def.privs or {} + def.mod_origin = core.get_current_modname() or "??" + core.registered_chatcommands[cmd] = def +end + +function core.unregister_chatcommand(name) + if core.registered_chatcommands[name] then + core.registered_chatcommands[name] = nil + else + core.log("warning", "Not unregistering chatcommand " ..name.. + " because it doesn't exist.") + end +end + +function core.override_chatcommand(name, redefinition) + local chatcommand = core.registered_chatcommands[name] + assert(chatcommand, "Attempt to override non-existent chatcommand "..name) + for k, v in pairs(redefinition) do + rawset(chatcommand, k, v) + end + core.registered_chatcommands[name] = chatcommand +end + +local function format_help_line(cmd, def) + local cmd_marker = INIT == "client" and "." or "/" + local msg = core.colorize("#00ffff", cmd_marker .. cmd) + if def.params and def.params ~= "" then + msg = msg .. " " .. def.params + end + if def.description and def.description ~= "" then + msg = msg .. ": " .. def.description + end + return msg +end + +local function do_help_cmd(name, param) + local opts, args = getopts("help", param) + if not opts then + return false, args + end + if #args > 1 then + return false, S("Too many arguments, try using just /help ") + end + local use_gui = INIT ~= "client" and core.get_player_by_name(name) + use_gui = use_gui and not opts:find("t") + + if #args == 0 and not use_gui then + local cmds = {} + for cmd, def in pairs(core.registered_chatcommands) do + if INIT == "client" or core.check_player_privs(name, def.privs) then + cmds[#cmds + 1] = cmd + end + end + table.sort(cmds) + local msg + if INIT == "game" then + msg = S("Available commands: @1", + table.concat(cmds, " ")) .. "\n" + .. S("Use '/help ' to get more " + .. "information, or '/help all' to list " + .. "everything.") + else + msg = core.gettext("Available commands: ") + .. table.concat(cmds, " ") .. "\n" + .. core.gettext("Use '.help ' to get more " + .. "information, or '.help all' to list " + .. "everything.") + end + return true, msg + elseif #args == 0 or (args[1] == "all" and use_gui) then + core.show_general_help_formspec(name) + return true + elseif args[1] == "all" then + local cmds = {} + for cmd, def in pairs(core.registered_chatcommands) do + if INIT == "client" or core.check_player_privs(name, def.privs) then + cmds[#cmds + 1] = format_help_line(cmd, def) + end + end + table.sort(cmds) + local msg + if INIT == "game" then + msg = S("Available commands:") + else + msg = core.gettext("Available commands:") + end + return true, msg.."\n"..table.concat(cmds, "\n") + elseif INIT == "game" and args[1] == "privs" then + if use_gui then + core.show_privs_help_formspec(name) + return true + end + local privs = {} + for priv, def in pairs(core.registered_privileges) do + privs[#privs + 1] = priv .. ": " .. def.description + end + table.sort(privs) + return true, S("Available privileges:").."\n"..table.concat(privs, "\n") + else + local cmd = args[1] + local def = core.registered_chatcommands[cmd] + if not def then + local msg + if INIT == "game" then + msg = S("Command not available: @1", cmd) + else + msg = core.gettext("Command not available: ") .. cmd + end + return false, msg + else + return true, format_help_line(cmd, def) + end + end +end + +if INIT == "client" then + core.register_chatcommand("help", { + params = core.gettext("[all | ]"), + description = core.gettext("Get help for commands"), + func = function(param) + return do_help_cmd(nil, param) + end, + }) +else + core.register_chatcommand("help", { + params = S("[all | privs | ] [-t]"), + description = S("Get help for commands or list privileges (-t: output in chat)"), + func = do_help_cmd, + }) +end diff --git a/builtin/common/filterlist.lua b/builtin/common/filterlist.lua new file mode 100644 index 0000000..e30379f --- /dev/null +++ b/builtin/common/filterlist.lua @@ -0,0 +1,319 @@ +--Minetest +--Copyright (C) 2013 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-------------------------------------------------------------------------------- +-- TODO improve doc -- +-- TODO code cleanup -- +-- Generic implementation of a filter/sortable list -- +-- Usage: -- +-- Filterlist needs to be initialized on creation. To achieve this you need to -- +-- pass following functions: -- +-- raw_fct() (mandatory): -- +-- function returning a table containing the elements to be filtered -- +-- compare_fct(element1,element2) (mandatory): -- +-- function returning true/false if element1 is same element as element2 -- +-- uid_match_fct(element1,uid) (optional) -- +-- function telling if uid is attached to element1 -- +-- filter_fct(element,filtercriteria) (optional) -- +-- function returning true/false if filtercriteria met to element -- +-- fetch_param (optional) -- +-- parameter passed to raw_fct to aquire correct raw data -- +-- -- +-------------------------------------------------------------------------------- +filterlist = {} + +-------------------------------------------------------------------------------- +function filterlist.refresh(self) + self.m_raw_list = self.m_raw_list_fct(self.m_fetch_param) + filterlist.process(self) +end + +-------------------------------------------------------------------------------- +function filterlist.create(raw_fct,compare_fct,uid_match_fct,filter_fct,fetch_param) + + assert((raw_fct ~= nil) and (type(raw_fct) == "function")) + assert((compare_fct ~= nil) and (type(compare_fct) == "function")) + + local self = {} + + self.m_raw_list_fct = raw_fct + self.m_compare_fct = compare_fct + self.m_filter_fct = filter_fct + self.m_uid_match_fct = uid_match_fct + + self.m_filtercriteria = nil + self.m_fetch_param = fetch_param + + self.m_sortmode = "none" + self.m_sort_list = {} + + self.m_processed_list = nil + self.m_raw_list = self.m_raw_list_fct(self.m_fetch_param) + + self.add_sort_mechanism = filterlist.add_sort_mechanism + self.set_filtercriteria = filterlist.set_filtercriteria + self.get_filtercriteria = filterlist.get_filtercriteria + self.set_sortmode = filterlist.set_sortmode + self.get_list = filterlist.get_list + self.get_raw_list = filterlist.get_raw_list + self.get_raw_element = filterlist.get_raw_element + self.get_raw_index = filterlist.get_raw_index + self.get_current_index = filterlist.get_current_index + self.size = filterlist.size + self.uid_exists_raw = filterlist.uid_exists_raw + self.raw_index_by_uid = filterlist.raw_index_by_uid + self.refresh = filterlist.refresh + + filterlist.process(self) + + return self +end + +-------------------------------------------------------------------------------- +function filterlist.add_sort_mechanism(self,name,fct) + self.m_sort_list[name] = fct +end + +-------------------------------------------------------------------------------- +function filterlist.set_filtercriteria(self,criteria) + if criteria == self.m_filtercriteria and + type(criteria) ~= "table" then + return + end + self.m_filtercriteria = criteria + filterlist.process(self) +end + +-------------------------------------------------------------------------------- +function filterlist.get_filtercriteria(self) + return self.m_filtercriteria +end + +-------------------------------------------------------------------------------- +--supported sort mode "alphabetic|none" +function filterlist.set_sortmode(self,mode) + if (mode == self.m_sortmode) then + return + end + self.m_sortmode = mode + filterlist.process(self) +end + +-------------------------------------------------------------------------------- +function filterlist.get_list(self) + return self.m_processed_list +end + +-------------------------------------------------------------------------------- +function filterlist.get_raw_list(self) + return self.m_raw_list +end + +-------------------------------------------------------------------------------- +function filterlist.get_raw_element(self,idx) + if type(idx) ~= "number" then + idx = tonumber(idx) + end + + if idx ~= nil and idx > 0 and idx <= #self.m_raw_list then + return self.m_raw_list[idx] + end + + return nil +end + +-------------------------------------------------------------------------------- +function filterlist.get_raw_index(self,listindex) + assert(self.m_processed_list ~= nil) + + if listindex ~= nil and listindex > 0 and + listindex <= #self.m_processed_list then + local entry = self.m_processed_list[listindex] + + for i,v in ipairs(self.m_raw_list) do + + if self.m_compare_fct(v,entry) then + return i + end + end + end + + return 0 +end + +-------------------------------------------------------------------------------- +function filterlist.get_current_index(self,listindex) + assert(self.m_processed_list ~= nil) + + if listindex ~= nil and listindex > 0 and + listindex <= #self.m_raw_list then + local entry = self.m_raw_list[listindex] + + for i,v in ipairs(self.m_processed_list) do + + if self.m_compare_fct(v,entry) then + return i + end + end + end + + return 0 +end + +-------------------------------------------------------------------------------- +function filterlist.process(self) + assert(self.m_raw_list ~= nil) + + if self.m_sortmode == "none" and + self.m_filtercriteria == nil then + self.m_processed_list = self.m_raw_list + return + end + + self.m_processed_list = {} + + for k,v in pairs(self.m_raw_list) do + if self.m_filtercriteria == nil or + self.m_filter_fct(v,self.m_filtercriteria) then + self.m_processed_list[#self.m_processed_list + 1] = v + end + end + + if self.m_sortmode == "none" then + return + end + + if self.m_sort_list[self.m_sortmode] ~= nil and + type(self.m_sort_list[self.m_sortmode]) == "function" then + + self.m_sort_list[self.m_sortmode](self) + end +end + +-------------------------------------------------------------------------------- +function filterlist.size(self) + if self.m_processed_list == nil then + return 0 + end + + return #self.m_processed_list +end + +-------------------------------------------------------------------------------- +function filterlist.uid_exists_raw(self,uid) + for i,v in ipairs(self.m_raw_list) do + if self.m_uid_match_fct(v,uid) then + return true + end + end + return false +end + +-------------------------------------------------------------------------------- +function filterlist.raw_index_by_uid(self, uid) + local elementcount = 0 + local elementidx = 0 + for i,v in ipairs(self.m_raw_list) do + if self.m_uid_match_fct(v,uid) then + elementcount = elementcount +1 + elementidx = i + end + end + + + -- If there are more elements than one with same name uid can't decide which + -- one is meant. self shouldn't be possible but just for sure. + if elementcount > 1 then + elementidx=0 + end + + return elementidx +end + +-------------------------------------------------------------------------------- +-- COMMON helper functions -- +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +function compare_worlds(world1,world2) + if world1.path ~= world2.path then + return false + end + + if world1.name ~= world2.name then + return false + end + + if world1.gameid ~= world2.gameid then + return false + end + + return true +end + +-------------------------------------------------------------------------------- +function sort_worlds_alphabetic(self) + + table.sort(self.m_processed_list, function(a, b) + --fixes issue #857 (crash due to sorting nil in worldlist) + if a == nil or b == nil then + if a == nil and b ~= nil then return false end + if b == nil and a ~= nil then return true end + return false + end + if a.name:lower() == b.name:lower() then + return a.name < b.name + end + return a.name:lower() < b.name:lower() + end) +end + +-------------------------------------------------------------------------------- +function sort_mod_list(self) + + table.sort(self.m_processed_list, function(a, b) + -- Show game mods at bottom + if a.type ~= b.type or a.loc ~= b.loc then + if b.type == "game" then + return a.loc ~= "game" + end + return b.loc == "game" + end + -- If in same or no modpack, sort by name + if a.modpack == b.modpack then + if a.name:lower() == b.name:lower() then + return a.name < b.name + end + return a.name:lower() < b.name:lower() + -- Else compare name to modpack name + else + -- Always show modpack pseudo-mod on top of modpack mod list + if a.name == b.modpack then + return true + elseif b.name == a.modpack then + return false + end + + local name_a = a.modpack or a.name + local name_b = b.modpack or b.name + if name_a:lower() == name_b:lower() then + return name_a < name_b + end + return name_a:lower() < name_b:lower() + end + end) +end diff --git a/builtin/common/information_formspecs.lua b/builtin/common/information_formspecs.lua new file mode 100644 index 0000000..1445a01 --- /dev/null +++ b/builtin/common/information_formspecs.lua @@ -0,0 +1,136 @@ +local COLOR_BLUE = "#7AF" +local COLOR_GREEN = "#7F7" +local COLOR_GRAY = "#BBB" + +local LIST_FORMSPEC = [[ + size[13,6.5] + label[0,-0.1;%s] + tablecolumns[color;tree;text;text] + table[0,0.5;12.8,5.5;list;%s;0] + button_exit[5,6;3,1;quit;%s] + ]] + +local LIST_FORMSPEC_DESCRIPTION = [[ + size[13,7.5] + label[0,-0.1;%s] + tablecolumns[color;tree;text;text] + table[0,0.5;12.8,4.8;list;%s;%i] + box[0,5.5;12.8,1.5;#000] + textarea[0.3,5.5;13.05,1.9;;;%s] + button_exit[5,7;3,1;quit;%s] + ]] + +local F = core.formspec_escape +local S = core.get_translator("__builtin") + + +-- CHAT COMMANDS FORMSPEC + +local mod_cmds = {} + +local function load_mod_command_tree() + mod_cmds = {} + + for name, def in pairs(core.registered_chatcommands) do + mod_cmds[def.mod_origin] = mod_cmds[def.mod_origin] or {} + local cmds = mod_cmds[def.mod_origin] + + -- Could be simplified, but avoid the priv checks whenever possible + cmds[#cmds + 1] = { name, def } + end + local sorted_mod_cmds = {} + for modname, cmds in pairs(mod_cmds) do + table.sort(cmds, function(a, b) return a[1] < b[1] end) + sorted_mod_cmds[#sorted_mod_cmds + 1] = { modname, cmds } + end + table.sort(sorted_mod_cmds, function(a, b) return a[1] < b[1] end) + mod_cmds = sorted_mod_cmds +end + +core.after(0, load_mod_command_tree) + +local function build_chatcommands_formspec(name, sel, copy) + local rows = {} + rows[1] = "#FFF,0,"..F(S("Command"))..","..F(S("Parameters")) + + local description = S("For more information, click on " + .. "any entry in the list.").. "\n" .. + S("Double-click to copy the entry to the chat history.") + + local privs = core.get_player_privs(name) + for i, data in ipairs(mod_cmds) do + rows[#rows + 1] = COLOR_BLUE .. ",0," .. F(data[1]) .. "," + for j, cmds in ipairs(data[2]) do + local has_priv = privs[cmds[2].privs] + rows[#rows + 1] = ("%s,1,%s,%s"):format( + has_priv and COLOR_GREEN or COLOR_GRAY, + cmds[1], F(cmds[2].params)) + if sel == #rows then + description = cmds[2].description + if copy then + core.chat_send_player(name, S("Command: @1 @2", + core.colorize("#0FF", "/" .. cmds[1]), cmds[2].params)) + end + end + end + end + + return LIST_FORMSPEC_DESCRIPTION:format( + F(S("Available commands: (see also: /help )")), + table.concat(rows, ","), sel or 0, + F(description), F(S("Close")) + ) +end + + +-- PRIVILEGES FORMSPEC + +local function build_privs_formspec(name) + local privs = {} + for priv_name, def in pairs(core.registered_privileges) do + privs[#privs + 1] = { priv_name, def } + end + table.sort(privs, function(a, b) return a[1] < b[1] end) + + local rows = {} + rows[1] = "#FFF,0,"..F(S("Privilege"))..","..F(S("Description")) + + local player_privs = core.get_player_privs(name) + for i, data in ipairs(privs) do + rows[#rows + 1] = ("%s,0,%s,%s"):format( + player_privs[data[1]] and COLOR_GREEN or COLOR_GRAY, + data[1], F(data[2].description)) + end + + return LIST_FORMSPEC:format( + F(S("Available privileges:")), + table.concat(rows, ","), + F(S("Close")) + ) +end + + +-- DETAILED CHAT COMMAND INFORMATION + +core.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= "__builtin:help_cmds" or fields.quit then + return + end + + local event = core.explode_table_event(fields.list) + if event.type ~= "INV" then + local name = player:get_player_name() + core.show_formspec(name, "__builtin:help_cmds", + build_chatcommands_formspec(name, event.row, event.type == "DCL")) + end +end) + +function core.show_general_help_formspec(name) + core.show_formspec(name, "__builtin:help_cmds", + build_chatcommands_formspec(name)) +end + +function core.show_privs_help_formspec(name) + core.show_formspec(name, "__builtin:help_privs", + build_privs_formspec(name)) +end diff --git a/builtin/common/misc_helpers.lua b/builtin/common/misc_helpers.lua new file mode 100644 index 0000000..467f188 --- /dev/null +++ b/builtin/common/misc_helpers.lua @@ -0,0 +1,770 @@ +-- Minetest: builtin/misc_helpers.lua + +-------------------------------------------------------------------------------- +-- Localize functions to avoid table lookups (better performance). +local string_sub, string_find = string.sub, string.find + +-------------------------------------------------------------------------------- +local function basic_dump(o) + local tp = type(o) + if tp == "number" then + return tostring(o) + elseif tp == "string" then + return string.format("%q", o) + elseif tp == "boolean" then + return tostring(o) + elseif tp == "nil" then + return "nil" + -- Uncomment for full function dumping support. + -- Not currently enabled because bytecode isn't very human-readable and + -- dump's output is intended for humans. + --elseif tp == "function" then + -- return string.format("loadstring(%q)", string.dump(o)) + elseif tp == "userdata" then + return tostring(o) + else + return string.format("<%s>", tp) + end +end + +local keywords = { + ["and"] = true, + ["break"] = true, + ["do"] = true, + ["else"] = true, + ["elseif"] = true, + ["end"] = true, + ["false"] = true, + ["for"] = true, + ["function"] = true, + ["goto"] = true, -- Lua 5.2 + ["if"] = true, + ["in"] = true, + ["local"] = true, + ["nil"] = true, + ["not"] = true, + ["or"] = true, + ["repeat"] = true, + ["return"] = true, + ["then"] = true, + ["true"] = true, + ["until"] = true, + ["while"] = true, +} +local function is_valid_identifier(str) + if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then + return false + end + return true +end + +-------------------------------------------------------------------------------- +-- Dumps values in a line-per-value format. +-- For example, {test = {"Testing..."}} becomes: +-- _["test"] = {} +-- _["test"][1] = "Testing..." +-- This handles tables as keys and circular references properly. +-- It also handles multiple references well, writing the table only once. +-- The dumped argument is internal-only. + +function dump2(o, name, dumped) + name = name or "_" + -- "dumped" is used to keep track of serialized tables to handle + -- multiple references and circular tables properly. + -- It only contains tables as keys. The value is the name that + -- the table has in the dump, eg: + -- {x = {"y"}} -> dumped[{"y"}] = '_["x"]' + dumped = dumped or {} + if type(o) ~= "table" then + return string.format("%s = %s\n", name, basic_dump(o)) + end + if dumped[o] then + return string.format("%s = %s\n", name, dumped[o]) + end + dumped[o] = name + -- This contains a list of strings to be concatenated later (because + -- Lua is slow at individual concatenation). + local t = {} + for k, v in pairs(o) do + local keyStr + if type(k) == "table" then + if dumped[k] then + keyStr = dumped[k] + else + -- Key tables don't have a name, so use one of + -- the form _G["table: 0xFFFFFFF"] + keyStr = string.format("_G[%q]", tostring(k)) + -- Dump key table + t[#t + 1] = dump2(k, keyStr, dumped) + end + else + keyStr = basic_dump(k) + end + local vname = string.format("%s[%s]", name, keyStr) + t[#t + 1] = dump2(v, vname, dumped) + end + return string.format("%s = {}\n%s", name, table.concat(t)) +end + +-------------------------------------------------------------------------------- +-- This dumps values in a one-statement format. +-- For example, {test = {"Testing..."}} becomes: +-- [[{ +-- test = { +-- "Testing..." +-- } +-- }]] +-- This supports tables as keys, but not circular references. +-- It performs poorly with multiple references as it writes out the full +-- table each time. +-- The indent field specifies a indentation string, it defaults to a tab. +-- Use the empty string to disable indentation. +-- The dumped and level arguments are internal-only. + +function dump(o, indent, nested, level) + local t = type(o) + if not level and t == "userdata" then + -- when userdata (e.g. player) is passed directly, print its metatable: + return "userdata metatable: " .. dump(getmetatable(o)) + end + if t ~= "table" then + return basic_dump(o) + end + + -- Contains table -> true/nil of currently nested tables + nested = nested or {} + if nested[o] then + return "" + end + nested[o] = true + indent = indent or "\t" + level = level or 1 + + local ret = {} + local dumped_indexes = {} + for i, v in ipairs(o) do + ret[#ret + 1] = dump(v, indent, nested, level + 1) + dumped_indexes[i] = true + end + for k, v in pairs(o) do + if not dumped_indexes[k] then + if type(k) ~= "string" or not is_valid_identifier(k) then + k = "["..dump(k, indent, nested, level + 1).."]" + end + v = dump(v, indent, nested, level + 1) + ret[#ret + 1] = k.." = "..v + end + end + nested[o] = nil + if indent ~= "" then + local indent_str = "\n"..string.rep(indent, level) + local end_indent_str = "\n"..string.rep(indent, level - 1) + return string.format("{%s%s%s}", + indent_str, + table.concat(ret, ","..indent_str), + end_indent_str) + end + return "{"..table.concat(ret, ", ").."}" +end + +-------------------------------------------------------------------------------- +function string.split(str, delim, include_empty, max_splits, sep_is_pattern) + delim = delim or "," + max_splits = max_splits or -2 + local items = {} + local pos, len = 1, #str + local plain = not sep_is_pattern + max_splits = max_splits + 1 + repeat + local np, npe = string_find(str, delim, pos, plain) + np, npe = (np or (len+1)), (npe or (len+1)) + if (not np) or (max_splits == 1) then + np = len + 1 + npe = np + end + local s = string_sub(str, pos, np - 1) + if include_empty or (s ~= "") then + max_splits = max_splits - 1 + items[#items + 1] = s + end + pos = npe + 1 + until (max_splits == 0) or (pos > (len + 1)) + return items +end + +-------------------------------------------------------------------------------- +function table.indexof(list, val) + for i, v in ipairs(list) do + if v == val then + return i + end + end + return -1 +end + +-------------------------------------------------------------------------------- +function string:trim() + return self:match("^%s*(.-)%s*$") +end + +-------------------------------------------------------------------------------- +function math.hypot(x, y) + return math.sqrt(x * x + y * y) +end + +-------------------------------------------------------------------------------- +function math.sign(x, tolerance) + tolerance = tolerance or 0 + if x > tolerance then + return 1 + elseif x < -tolerance then + return -1 + end + return 0 +end + +-------------------------------------------------------------------------------- +function math.factorial(x) + assert(x % 1 == 0 and x >= 0, "factorial expects a non-negative integer") + if x >= 171 then + -- 171! is greater than the biggest double, no need to calculate + return math.huge + end + local v = 1 + for k = 2, x do + v = v * k + end + return v +end + + +function math.round(x) + if x >= 0 then + return math.floor(x + 0.5) + end + return math.ceil(x - 0.5) +end + +local formspec_escapes = { + ["\\"] = "\\\\", + ["["] = "\\[", + ["]"] = "\\]", + [";"] = "\\;", + [","] = "\\," +} +function core.formspec_escape(text) + -- Use explicit character set instead of dot here because it doubles the performance + return text and string.gsub(text, "[\\%[%];,]", formspec_escapes) +end + + +function core.wrap_text(text, max_length, as_table) + local result = {} + local line = {} + if #text <= max_length then + return as_table and {text} or text + end + + local line_length = 0 + for word in text:gmatch("%S+") do + if line_length > 0 and line_length + #word + 1 >= max_length then + -- word wouldn't fit on current line, move to next line + table.insert(result, table.concat(line, " ")) + line = {word} + line_length = #word + else + table.insert(line, word) + line_length = line_length + 1 + #word + end + end + + table.insert(result, table.concat(line, " ")) + return as_table and result or table.concat(result, "\n") +end + +-------------------------------------------------------------------------------- + +if INIT == "game" then + local dirs1 = {9, 18, 7, 12} + local dirs2 = {20, 23, 22, 21} + + function core.rotate_and_place(itemstack, placer, pointed_thing, + infinitestacks, orient_flags, prevent_after_place) + orient_flags = orient_flags or {} + + local unode = core.get_node_or_nil(pointed_thing.under) + if not unode then + return + end + local undef = core.registered_nodes[unode.name] + local sneaking = placer and placer:get_player_control().sneak + if undef and undef.on_rightclick and not sneaking then + return undef.on_rightclick(pointed_thing.under, unode, placer, + itemstack, pointed_thing) + end + local fdir = placer and core.dir_to_facedir(placer:get_look_dir()) or 0 + + local above = pointed_thing.above + local under = pointed_thing.under + local iswall = (above.y == under.y) + local isceiling = not iswall and (above.y < under.y) + + if undef and undef.buildable_to then + iswall = false + end + + if orient_flags.force_floor then + iswall = false + isceiling = false + elseif orient_flags.force_ceiling then + iswall = false + isceiling = true + elseif orient_flags.force_wall then + iswall = true + isceiling = false + elseif orient_flags.invert_wall then + iswall = not iswall + end + + local param2 = fdir + if iswall then + param2 = dirs1[fdir + 1] + elseif isceiling then + if orient_flags.force_facedir then + param2 = 20 + else + param2 = dirs2[fdir + 1] + end + else -- place right side up + if orient_flags.force_facedir then + param2 = 0 + end + end + + local old_itemstack = ItemStack(itemstack) + local new_itemstack = core.item_place_node(itemstack, placer, + pointed_thing, param2, prevent_after_place) + return infinitestacks and old_itemstack or new_itemstack + end + + +-------------------------------------------------------------------------------- +--Wrapper for rotate_and_place() to check for sneak and assume Creative mode +--implies infinite stacks when performing a 6d rotation. +-------------------------------------------------------------------------------- + core.rotate_node = function(itemstack, placer, pointed_thing) + local name = placer and placer:get_player_name() or "" + local invert_wall = placer and placer:get_player_control().sneak or false + return core.rotate_and_place(itemstack, placer, pointed_thing, + core.is_creative_enabled(name), + {invert_wall = invert_wall}, true) + end +end + +-------------------------------------------------------------------------------- +function core.explode_table_event(evt) + if evt ~= nil then + local parts = evt:split(":") + if #parts == 3 then + local t = parts[1]:trim() + local r = tonumber(parts[2]:trim()) + local c = tonumber(parts[3]:trim()) + if type(r) == "number" and type(c) == "number" + and t ~= "INV" then + return {type=t, row=r, column=c} + end + end + end + return {type="INV", row=0, column=0} +end + +-------------------------------------------------------------------------------- +function core.explode_textlist_event(evt) + if evt ~= nil then + local parts = evt:split(":") + if #parts == 2 then + local t = parts[1]:trim() + local r = tonumber(parts[2]:trim()) + if type(r) == "number" and t ~= "INV" then + return {type=t, index=r} + end + end + end + return {type="INV", index=0} +end + +-------------------------------------------------------------------------------- +function core.explode_scrollbar_event(evt) + local retval = core.explode_textlist_event(evt) + + retval.value = retval.index + retval.index = nil + + return retval +end + +-------------------------------------------------------------------------------- +function core.rgba(r, g, b, a) + return a and string.format("#%02X%02X%02X%02X", r, g, b, a) or + string.format("#%02X%02X%02X", r, g, b) +end + +-------------------------------------------------------------------------------- +function core.pos_to_string(pos, decimal_places) + local x = pos.x + local y = pos.y + local z = pos.z + if decimal_places ~= nil then + x = string.format("%." .. decimal_places .. "f", x) + y = string.format("%." .. decimal_places .. "f", y) + z = string.format("%." .. decimal_places .. "f", z) + end + return "(" .. x .. "," .. y .. "," .. z .. ")" +end + +-------------------------------------------------------------------------------- +function core.string_to_pos(value) + if value == nil then + return nil + end + + value = value:match("^%((.-)%)$") or value -- strip parentheses + + local x, y, z = value:trim():match("^([%d.-]+)[,%s]%s*([%d.-]+)[,%s]%s*([%d.-]+)$") + if x and y and z then + x = tonumber(x) + y = tonumber(y) + z = tonumber(z) + return vector.new(x, y, z) + end + + return nil +end + + +-------------------------------------------------------------------------------- + +do + local rel_num_cap = "(~?-?%d*%.?%d*)" -- may be overly permissive as this will be tonumber'ed anyways + local num_delim = "[,%s]%s*" + local pattern = "^" .. table.concat({rel_num_cap, rel_num_cap, rel_num_cap}, num_delim) .. "$" + + local function parse_area_string(pos, relative_to) + local pp = {} + pp.x, pp.y, pp.z = pos:trim():match(pattern) + return core.parse_coordinates(pp.x, pp.y, pp.z, relative_to) + end + + function core.string_to_area(value, relative_to) + local p1, p2 = value:match("^%((.-)%)%s*%((.-)%)$") + if not p1 then + return + end + + p1 = parse_area_string(p1, relative_to) + p2 = parse_area_string(p2, relative_to) + + if p1 == nil or p2 == nil then + return + end + + return p1, p2 + end +end + +-------------------------------------------------------------------------------- +function table.copy(t, seen) + local n = {} + seen = seen or {} + seen[t] = n + for k, v in pairs(t) do + n[(type(k) == "table" and (seen[k] or table.copy(k, seen))) or k] = + (type(v) == "table" and (seen[v] or table.copy(v, seen))) or v + end + return n +end + + +function table.insert_all(t, other) + for i=1, #other do + t[#t + 1] = other[i] + end + return t +end + + +function table.key_value_swap(t) + local ti = {} + for k,v in pairs(t) do + ti[v] = k + end + return ti +end + + +function table.shuffle(t, from, to, random) + from = from or 1 + to = to or #t + random = random or math.random + local n = to - from + 1 + while n > 1 do + local r = from + n-1 + local l = from + random(0, n-1) + t[l], t[r] = t[r], t[l] + n = n-1 + end +end + + +-------------------------------------------------------------------------------- +-- mainmenu only functions +-------------------------------------------------------------------------------- +if INIT == "mainmenu" then + function core.get_game(index) + local games = core.get_games() + + if index > 0 and index <= #games then + return games[index] + end + + return nil + end +end + +if core.gettext then -- for client and mainmenu + function fgettext_ne(text, ...) + text = core.gettext(text) + local arg = {n=select('#', ...), ...} + if arg.n >= 1 then + -- Insert positional parameters ($1, $2, ...) + local result = '' + local pos = 1 + while pos <= text:len() do + local newpos = text:find('[$]', pos) + if newpos == nil then + result = result .. text:sub(pos) + pos = text:len() + 1 + else + local paramindex = + tonumber(text:sub(newpos+1, newpos+1)) + result = result .. text:sub(pos, newpos-1) + .. tostring(arg[paramindex]) + pos = newpos + 2 + end + end + text = result + end + return text + end + + function fgettext(text, ...) + return core.formspec_escape(fgettext_ne(text, ...)) + end +end + +local ESCAPE_CHAR = string.char(0x1b) + +function core.get_color_escape_sequence(color) + return ESCAPE_CHAR .. "(c@" .. color .. ")" +end + +function core.get_background_escape_sequence(color) + return ESCAPE_CHAR .. "(b@" .. color .. ")" +end + +function core.colorize(color, message) + local lines = tostring(message):split("\n", true) + local color_code = core.get_color_escape_sequence(color) + + for i, line in ipairs(lines) do + lines[i] = color_code .. line + end + + return table.concat(lines, "\n") .. core.get_color_escape_sequence("#ffffff") +end + + +function core.strip_foreground_colors(str) + return (str:gsub(ESCAPE_CHAR .. "%(c@[^)]+%)", "")) +end + +function core.strip_background_colors(str) + return (str:gsub(ESCAPE_CHAR .. "%(b@[^)]+%)", "")) +end + +function core.strip_colors(str) + return (str:gsub(ESCAPE_CHAR .. "%([bc]@[^)]+%)", "")) +end + +function core.translate(textdomain, str, ...) + local start_seq + if textdomain == "" then + start_seq = ESCAPE_CHAR .. "T" + else + start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")" + end + local arg = {n=select('#', ...), ...} + local end_seq = ESCAPE_CHAR .. "E" + local arg_index = 1 + local translated = str:gsub("@(.)", function(matched) + local c = string.byte(matched) + if string.byte("1") <= c and c <= string.byte("9") then + local a = c - string.byte("0") + if a ~= arg_index then + error("Escape sequences in string given to core.translate " .. + "are not in the correct order: got @" .. matched .. + "but expected @" .. tostring(arg_index)) + end + if a > arg.n then + error("Not enough arguments provided to core.translate") + end + arg_index = arg_index + 1 + return ESCAPE_CHAR .. "F" .. arg[a] .. ESCAPE_CHAR .. "E" + elseif matched == "n" then + return "\n" + else + return matched + end + end) + if arg_index < arg.n + 1 then + error("Too many arguments provided to core.translate") + end + return start_seq .. translated .. end_seq +end + +function core.get_translator(textdomain) + return function(str, ...) return core.translate(textdomain or "", str, ...) end +end + +-------------------------------------------------------------------------------- +-- Returns the exact coordinate of a pointed surface +-------------------------------------------------------------------------------- +function core.pointed_thing_to_face_pos(placer, pointed_thing) + -- Avoid crash in some situations when player is inside a node, causing + -- 'above' to equal 'under'. + if vector.equals(pointed_thing.above, pointed_thing.under) then + return pointed_thing.under + end + + local eye_height = placer:get_properties().eye_height + local eye_offset_first = placer:get_eye_offset() + local node_pos = pointed_thing.under + local camera_pos = placer:get_pos() + local pos_off = vector.multiply( + vector.subtract(pointed_thing.above, node_pos), 0.5) + local look_dir = placer:get_look_dir() + local offset, nc + local oc = {} + + for c, v in pairs(pos_off) do + if nc or v == 0 then + oc[#oc + 1] = c + else + offset = v + nc = c + end + end + + local fine_pos = {[nc] = node_pos[nc] + offset} + camera_pos.y = camera_pos.y + eye_height + eye_offset_first.y / 10 + local f = (node_pos[nc] + offset - camera_pos[nc]) / look_dir[nc] + + for i = 1, #oc do + fine_pos[oc[i]] = camera_pos[oc[i]] + look_dir[oc[i]] * f + end + return fine_pos +end + +function core.string_to_privs(str, delim) + assert(type(str) == "string") + delim = delim or ',' + local privs = {} + for _, priv in pairs(string.split(str, delim)) do + privs[priv:trim()] = true + end + return privs +end + +function core.privs_to_string(privs, delim) + assert(type(privs) == "table") + delim = delim or ',' + local list = {} + for priv, bool in pairs(privs) do + if bool then + list[#list + 1] = priv + end + end + return table.concat(list, delim) +end + +function core.is_nan(number) + return number ~= number +end + +--[[ Helper function for parsing an optionally relative number +of a chat command parameter, using the chat command tilde notation. + +Parameters: +* arg: String snippet containing the number; possible values: + * "": return as number + * "~": return relative_to + + * "~": return relative_to + * Anything else will return `nil` +* relative_to: Number to which the `arg` number might be relative to + +Returns: +A number or `nil`, depending on `arg. + +Examples: +* `core.parse_relative_number("5", 10)` returns 5 +* `core.parse_relative_number("~5", 10)` returns 15 +* `core.parse_relative_number("~", 10)` returns 10 +]] +function core.parse_relative_number(arg, relative_to) + if not arg then + return nil + elseif arg == "~" then + return relative_to + elseif string.sub(arg, 1, 1) == "~" then + local number = tonumber(string.sub(arg, 2)) + if not number then + return nil + end + if core.is_nan(number) or number == math.huge or number == -math.huge then + return nil + end + return relative_to + number + else + local number = tonumber(arg) + if core.is_nan(number) or number == math.huge or number == -math.huge then + return nil + end + return number + end +end + +--[[ Helper function to parse coordinates that might be relative +to another position; supports chat command tilde notation. +Intended to be used in chat command parameter parsing. + +Parameters: +* x, y, z: Parsed x, y, and z coordinates as strings +* relative_to: Position to which to compare the position + +Syntax of x, y and z: +* "": return as number +* "~": return + player position on this axis +* "~": return player position on this axis + +Returns: a vector or nil for invalid input or if player does not exist +]] +function core.parse_coordinates(x, y, z, relative_to) + if not relative_to then + x, y, z = tonumber(x), tonumber(y), tonumber(z) + return x and y and z and { x = x, y = y, z = z } + end + local rx = core.parse_relative_number(x, relative_to.x) + local ry = core.parse_relative_number(y, relative_to.y) + local rz = core.parse_relative_number(z, relative_to.z) + return rx and ry and rz and { x = rx, y = ry, z = rz } +end diff --git a/builtin/common/mod_storage.lua b/builtin/common/mod_storage.lua new file mode 100644 index 0000000..7ccf629 --- /dev/null +++ b/builtin/common/mod_storage.lua @@ -0,0 +1,19 @@ +-- Modify core.get_mod_storage to return the storage for the current mod. + +local get_current_modname = core.get_current_modname + +local old_get_mod_storage = core.get_mod_storage + +local storages = setmetatable({}, { + __mode = "v", -- values are weak references (can be garbage-collected) + __index = function(self, modname) + local storage = old_get_mod_storage(modname) + self[modname] = storage + return storage + end, +}) + +function core.get_mod_storage() + local modname = get_current_modname() + return modname and storages[modname] +end diff --git a/builtin/common/serialize.lua b/builtin/common/serialize.lua new file mode 100644 index 0000000..afa63b3 --- /dev/null +++ b/builtin/common/serialize.lua @@ -0,0 +1,238 @@ +--- Lua module to serialize values as Lua code. +-- From: https://github.com/appgurueu/modlib/blob/master/luon.lua +-- License: MIT + +local next, rawget, pairs, pcall, error, type, setfenv, loadstring + = next, rawget, pairs, pcall, error, type, setfenv, loadstring + +local table_concat, string_dump, string_format, string_match, math_huge + = table.concat, string.dump, string.format, string.match, math.huge + +-- Recursively counts occurences of objects (non-primitives including strings) in a table. +local function count_objects(value) + local counts = {} + if value == nil then + -- Early return for nil; tables can't contain nil + return counts + end + local function count_values(val) + local type_ = type(val) + if type_ == "boolean" or type_ == "number" then + return + end + local count = counts[val] + counts[val] = (count or 0) + 1 + if type_ == "table" then + if not count then + for k, v in pairs(val) do + count_values(k) + count_values(v) + end + end + elseif type_ ~= "string" and type_ ~= "function" then + error("unsupported type: " .. type_) + end + end + count_values(value) + return counts +end + +-- Build a "set" of Lua keywords. These can't be used as short key names. +-- See https://www.lua.org/manual/5.1/manual.html#2.1 +local keywords = {} +for _, keyword in pairs({ + "and", "break", "do", "else", "elseif", + "end", "false", "for", "function", "if", + "in", "local", "nil", "not", "or", + "repeat", "return", "then", "true", "until", "while", + "goto" -- LuaJIT, Lua 5.2+ +}) do + keywords[keyword] = true +end + +local function quote(string) + return string_format("%q", string) +end + +local function dump_func(func) + return string_format("loadstring(%q)", string_dump(func)) +end + +-- Serializes Lua nil, booleans, numbers, strings, tables and even functions +-- Tables are referenced by reference, strings are referenced by value. Supports circular tables. +local function serialize(value, write) + local reference, refnum = "1", 1 + -- [object] = reference + local references = {} + -- Circular tables that must be filled using `table[key] = value` statements + local to_fill = {} + for object, count in pairs(count_objects(value)) do + local type_ = type(object) + -- Object must appear more than once. If it is a string, the reference has to be shorter than the string. + if count >= 2 and (type_ ~= "string" or #reference + 5 < #object) then + if refnum == 1 then + write"local _={};" -- initialize reference table + end + write"_[" + write(reference) + write("]=") + if type_ == "table" then + write("{}") + elseif type_ == "function" then + write(dump_func(object)) + elseif type_ == "string" then + write(quote(object)) + end + write(";") + references[object] = reference + if type_ == "table" then + to_fill[object] = reference + end + refnum = refnum + 1 + reference = ("%d"):format(refnum) + end + end + -- Used to decide whether we should do "key=..." + local function use_short_key(key) + return not references[key] and type(key) == "string" and (not keywords[key]) and string_match(key, "^[%a_][%a%d_]*$") + end + local function dump(value) + -- Primitive types + if value == nil then + return write("nil") + end + if value == true then + return write("true") + end + if value == false then + return write("false") + end + local type_ = type(value) + if type_ == "number" then + if value ~= value then -- nan + return write"0/0" + elseif value == math_huge then + return write"1/0" + elseif value == -math_huge then + return write"-1/0" + else + return write(string_format("%.17g", value)) + end + end + -- Reference types: table, function and string + local ref = references[value] + if ref then + write"_[" + write(ref) + return write"]" + end + if type_ == "string" then + return write(quote(value)) + end + if type_ == "function" then + return write(dump_func(value)) + end + if type_ == "table" then + write("{") + -- First write list keys: + -- Don't use the table length #value here as it may horribly fail + -- for tables which use large integers as keys in the hash part; + -- stop at the first "hole" (nil value) instead + local len = 0 + local first = true -- whether this is the first entry, which may not have a leading comma + while true do + local v = rawget(value, len + 1) -- use rawget to avoid metatables like the vector metatable + if v == nil then break end + if first then first = false else write(",") end + dump(v) + len = len + 1 + end + -- Now write map keys ([key] = value) + for k, v in next, value do + -- We have written all non-float keys in [1, len] already + if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > len then + if first then first = false else write(",") end + if use_short_key(k) then + write(k) + else + write("[") + dump(k) + write("]") + end + write("=") + dump(v) + end + end + write("}") + return + end + end + -- Write the statements to fill circular tables + for table, ref in pairs(to_fill) do + for k, v in pairs(table) do + write("_[") + write(ref) + write("]") + if use_short_key(k) then + write(".") + write(k) + else + write("[") + dump(k) + write("]") + end + write("=") + dump(v) + write(";") + end + end + write("return ") + dump(value) +end + +function core.serialize(value) + local rope = {} + serialize(value, function(text) + -- Faster than table.insert(rope, text) on PUC Lua 5.1 + rope[#rope + 1] = text + end) + return table_concat(rope) +end + +local function dummy_func() end + +function core.deserialize(str, safe) + -- Backwards compatibility + if str == nil then + core.log("deprecated", "minetest.deserialize called with nil (expected string).") + return nil, "Invalid type: Expected a string, got nil" + end + local t = type(str) + if t ~= "string" then + error(("minetest.deserialize called with %s (expected string)."):format(t)) + end + + local func, err = loadstring(str) + if not func then return nil, err end + + -- math.huge was serialized to inf and NaNs to nan by Lua in Minetest 5.6, so we have to support this here + local env = {inf = math_huge, nan = 0/0} + if safe then + env.loadstring = dummy_func + else + env.loadstring = function(str, ...) + local func, err = loadstring(str, ...) + if func then + setfenv(func, env) + return func + end + return nil, err + end + end + setfenv(func, env) + local success, value_or_err = pcall(func) + if success then + return value_or_err + end + return nil, value_or_err +end diff --git a/builtin/common/strict.lua b/builtin/common/strict.lua new file mode 100644 index 0000000..936ebb3 --- /dev/null +++ b/builtin/common/strict.lua @@ -0,0 +1,46 @@ +local getinfo, rawget, rawset = debug.getinfo, rawget, rawset + +function core.global_exists(name) + if type(name) ~= "string" then + error("core.global_exists: " .. tostring(name) .. " is not a string") + end + return rawget(_G, name) ~= nil +end + + +local meta = {} +local declared = {} +-- Key is source file, line, and variable name; seperated by NULs +local warned = {} + +function meta:__newindex(name, value) + if declared[name] then + return + end + local info = getinfo(2, "Sl") + local desc = ("%s:%d"):format(info.short_src, info.currentline) + local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name) + if not warned[warn_key] and info.what ~= "main" and info.what ~= "C" then + core.log("warning", ("Assignment to undeclared global %q inside a function at %s.") + :format(name, desc)) + warned[warn_key] = true + end + rawset(self, name, value) + declared[name] = true +end + + +function meta:__index(name) + if declared[name] then + return + end + local info = getinfo(2, "Sl") + local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name) + if not warned[warn_key] and info.what ~= "C" then + core.log("warning", ("Undeclared global variable %q accessed at %s:%s") + :format(name, info.short_src, info.currentline)) + warned[warn_key] = true + end +end + +setmetatable(_G, meta) diff --git a/builtin/common/tests/misc_helpers_spec.lua b/builtin/common/tests/misc_helpers_spec.lua new file mode 100644 index 0000000..7d046d5 --- /dev/null +++ b/builtin/common/tests/misc_helpers_spec.lua @@ -0,0 +1,173 @@ +_G.core = {} +_G.vector = {metatable = {}} +dofile("builtin/common/vector.lua") +dofile("builtin/common/misc_helpers.lua") + +describe("string", function() + it("trim()", function() + assert.equal("foo bar", string.trim("\n \t\tfoo bar\t ")) + end) + + describe("split()", function() + it("removes empty", function() + assert.same({ "hello" }, string.split("hello")) + assert.same({ "hello", "world" }, string.split("hello,world")) + assert.same({ "hello", "world" }, string.split("hello,world,,,")) + assert.same({ "hello", "world" }, string.split(",,,hello,world")) + assert.same({ "hello", "world", "2" }, string.split("hello,,,world,2")) + assert.same({ "hello ", " world" }, string.split("hello :| world", ":|")) + end) + + it("keeps empty", function() + assert.same({ "hello" }, string.split("hello", ",", true)) + assert.same({ "hello", "world" }, string.split("hello,world", ",", true)) + assert.same({ "hello", "world", "" }, string.split("hello,world,", ",", true)) + assert.same({ "hello", "", "", "world", "2" }, string.split("hello,,,world,2", ",", true)) + assert.same({ "", "", "hello", "world", "2" }, string.split(",,hello,world,2", ",", true)) + assert.same({ "hello ", " world | :" }, string.split("hello :| world | :", ":|")) + end) + + it("max_splits", function() + assert.same({ "one" }, string.split("one", ",", true, 2)) + assert.same({ "one,two,three,four" }, string.split("one,two,three,four", ",", true, 0)) + assert.same({ "one", "two", "three,four" }, string.split("one,two,three,four", ",", true, 2)) + assert.same({ "one", "", "two,three,four" }, string.split("one,,two,three,four", ",", true, 2)) + assert.same({ "one", "two", "three,four" }, string.split("one,,,,,,two,three,four", ",", false, 2)) + end) + + it("pattern", function() + assert.same({ "one", "two" }, string.split("one,two", ",", false, -1, true)) + assert.same({ "one", "two", "three" }, string.split("one2two3three", "%d", false, -1, true)) + end) + end) +end) + +describe("privs", function() + it("from string", function() + assert.same({ a = true, b = true }, core.string_to_privs("a,b")) + end) + + it("to string", function() + assert.equal("one", core.privs_to_string({ one=true })) + + local ret = core.privs_to_string({ a=true, b=true }) + assert(ret == "a,b" or ret == "b,a") + end) +end) + +describe("pos", function() + it("from string", function() + assert.equal(vector.new(10, 5.1, -2), core.string_to_pos("10.0, 5.1, -2")) + assert.equal(vector.new(10, 5.1, -2), core.string_to_pos("( 10.0, 5.1, -2)")) + assert.is_nil(core.string_to_pos("asd, 5, -2)")) + end) + + it("to string", function() + assert.equal("(10.1,5.2,-2.3)", core.pos_to_string({ x = 10.1, y = 5.2, z = -2.3})) + end) +end) + +describe("area parsing", function() + describe("valid inputs", function() + it("accepts absolute numbers", function() + local p1, p2 = core.string_to_area("(10.0, 5, -2) ( 30.2 4 -12.53)") + assert(p1.x == 10 and p1.y == 5 and p1.z == -2) + assert(p2.x == 30.2 and p2.y == 4 and p2.z == -12.53) + end) + + it("accepts relative numbers", function() + local p1, p2 = core.string_to_area("(1,2,3) (~5,~-5,~)", {x=10,y=10,z=10}) + assert(type(p1) == "table" and type(p2) == "table") + assert(p1.x == 1 and p1.y == 2 and p1.z == 3) + assert(p2.x == 15 and p2.y == 5 and p2.z == 10) + + p1, p2 = core.string_to_area("(1 2 3) (~5 ~-5 ~)", {x=10,y=10,z=10}) + assert(type(p1) == "table" and type(p2) == "table") + assert(p1.x == 1 and p1.y == 2 and p1.z == 3) + assert(p2.x == 15 and p2.y == 5 and p2.z == 10) + end) + end) + describe("invalid inputs", function() + it("rejects too few numbers", function() + local p1, p2 = core.string_to_area("(1,1) (1,1,1,1)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + end) + + it("rejects too many numbers", function() + local p1, p2 = core.string_to_area("(1,1,1,1) (1,1,1,1)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + end) + + it("rejects nan & inf", function() + local p1, p2 = core.string_to_area("(1,1,1) (1,1,nan)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(1,1,1) (1,1,~nan)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(1,1,1) (1,~nan,1)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(1,1,1) (1,1,inf)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(1,1,1) (1,1,~inf)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(1,1,1) (1,~inf,1)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(nan,nan,nan) (nan,nan,nan)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(nan,nan,nan) (nan,nan,nan)") + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(inf,inf,inf) (-inf,-inf,-inf)", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(inf,inf,inf) (-inf,-inf,-inf)") + assert(p1 == nil and p2 == nil) + end) + + it("rejects words", function() + local p1, p2 = core.string_to_area("bananas", {x=1,y=1,z=1}) + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("bananas", "foobar") + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("bananas") + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(bananas,bananas,bananas)") + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(bananas,bananas,bananas) (bananas,bananas,bananas)") + assert(p1 == nil and p2 == nil) + end) + + it("requires parenthesis & valid numbers", function() + local p1, p2 = core.string_to_area("(10.0, 5, -2 30.2, 4, -12.53") + assert(p1 == nil and p2 == nil) + + p1, p2 = core.string_to_area("(10.0, 5,) -2 fgdf2, 4, -12.53") + assert(p1 == nil and p2 == nil) + end) + end) +end) + +describe("table", function() + it("indexof()", function() + assert.equal(1, table.indexof({"foo", "bar"}, "foo")) + assert.equal(-1, table.indexof({"foo", "bar"}, "baz")) + end) +end) + +describe("formspec_escape", function() + it("escapes", function() + assert.equal(nil, core.formspec_escape(nil)) + assert.equal("", core.formspec_escape("")) + assert.equal("\\[Hello\\\\\\[", core.formspec_escape("[Hello\\[")) + end) +end) diff --git a/builtin/common/tests/serialize_spec.lua b/builtin/common/tests/serialize_spec.lua new file mode 100644 index 0000000..340e226 --- /dev/null +++ b/builtin/common/tests/serialize_spec.lua @@ -0,0 +1,189 @@ +_G.core = {} +_G.vector = {metatable = {}} + +_G.setfenv = require 'busted.compatibility'.setfenv + +dofile("builtin/common/serialize.lua") +dofile("builtin/common/vector.lua") + +-- Supports circular tables; does not support table keys +-- Correctly checks whether a mapping of references ("same") exists +-- Is significantly more efficient than assert.same +local function assert_same(a, b, same) + same = same or {} + if same[a] or same[b] then + assert(same[a] == b and same[b] == a) + return + end + if a == b then + return + end + if type(a) ~= "table" or type(b) ~= "table" then + assert(a == b) + return + end + same[a] = b + same[b] = a + local count = 0 + for k, v in pairs(a) do + count = count + 1 + assert(type(k) ~= "table") + assert_same(v, b[k], same) + end + for _ in pairs(b) do + count = count - 1 + end + assert(count == 0) +end + +local x, y = {}, {} +local t1, t2 = {x, x, y, y}, {x, y, x, y} +assert.same(t1, t2) -- will succeed because it only checks whether the depths match +assert(not pcall(assert_same, t1, t2)) -- will correctly fail because it checks whether the refs match + +describe("serialize", function() + local function assert_preserves(value) + local preserved_value = core.deserialize(core.serialize(value)) + assert_same(value, preserved_value) + end + it("works", function() + assert_preserves({cat={sound="nyan", speed=400}, dog={sound="woof"}}) + end) + + it("handles characters", function() + assert_preserves({escape_chars="\n\r\t\v\\\"\'", non_european="θשׁ٩∂"}) + end) + + it("handles NaN & infinities", function() + local nan = core.deserialize(core.serialize(0/0)) + assert(nan ~= nan) + assert_preserves(math.huge) + assert_preserves(-math.huge) + end) + + it("handles precise numbers", function() + assert_preserves(0.2695949158945771) + end) + + it("handles big integers", function() + assert_preserves(269594915894577) + end) + + it("handles recursive structures", function() + local test_in = { hello = "world" } + test_in.foo = test_in + assert_preserves(test_in) + end) + + it("handles cross-referencing structures", function() + local test_in = { + foo = { + baz = { + {} + }, + }, + bar = { + baz = {}, + }, + } + + test_in.foo.baz[1].foo = test_in.foo + test_in.foo.baz[1].bar = test_in.bar + test_in.bar.baz[1] = test_in.foo.baz[1] + + assert_preserves(test_in) + end) + + it("strips functions in safe mode", function() + local test_in = { + func = function(a, b) + error("test") + end, + foo = "bar" + } + setfenv(test_in.func, _G) + + local str = core.serialize(test_in) + assert.not_nil(str:find("loadstring")) + + local test_out = core.deserialize(str, true) + assert.is_nil(test_out.func) + assert.equals(test_out.foo, "bar") + end) + + it("vectors work", function() + local v = vector.new(1, 2, 3) + assert_preserves({v}) + assert_preserves(v) + + -- abuse + v = vector.new(1, 2, 3) + v.a = "bla" + assert_preserves(v) + end) + + it("handles keywords as keys", function() + assert_preserves({["and"] = "keyword", ["for"] = "keyword"}) + end) + + describe("fuzzing", function() + local atomics = {true, false, math.huge, -math.huge} -- no NaN or nil + local function atomic() + return atomics[math.random(1, #atomics)] + end + local function num() + local sign = math.random() < 0.5 and -1 or 1 + -- HACK math.random(a, b) requires a, b & b - a to fit within a 32-bit int + -- Use two random calls to generate a random number from 0 - 2^50 as lower & upper 25 bits + local val = math.random(0, 2^25) * 2^25 + math.random(0, 2^25 - 1) + local exp = math.random() < 0.5 and 1 or 2^(math.random(-120, 120)) + return sign * val * exp + end + local function charcodes(count) + if count == 0 then return end + return math.random(0, 0xFF), charcodes(count - 1) + end + local function str() + return string.char(charcodes(math.random(0, 100))) + end + local primitives = {atomic, num, str} + local function primitive() + return primitives[math.random(1, #primitives)]() + end + local function tab(max_actions) + local root = {} + local tables = {root} + local function random_table() + return tables[math.random(1, #tables)] + end + for _ = 1, math.random(1, max_actions) do + local tab = random_table() + local value + if math.random() < 0.5 then + if math.random() < 0.5 then + value = random_table() + else + value = {} + table.insert(tables, value) + end + else + value = primitive() + end + tab[math.random() < 0.5 and (#tab + 1) or primitive()] = value + end + return root + end + it("primitives work", function() + for _ = 1, 1e3 do + assert_preserves(primitive()) + end + end) + it("tables work", function() + for _ = 1, 100 do + local fuzzed_table = tab(1e3) + assert_same(fuzzed_table, table.copy(fuzzed_table)) + assert_preserves(fuzzed_table) + end + end) + end) +end) diff --git a/builtin/common/tests/vector_spec.lua b/builtin/common/tests/vector_spec.lua new file mode 100644 index 0000000..6a0b81a --- /dev/null +++ b/builtin/common/tests/vector_spec.lua @@ -0,0 +1,465 @@ +_G.vector = {metatable = {}} +dofile("builtin/common/vector.lua") + +describe("vector", function() + describe("new()", function() + it("constructs", function() + assert.same({x = 0, y = 0, z = 0}, vector.new()) + assert.same({x = 1, y = 2, z = 3}, vector.new(1, 2, 3)) + assert.same({x = 3, y = 2, z = 1}, vector.new({x = 3, y = 2, z = 1})) + + assert.is_true(vector.check(vector.new())) + assert.is_true(vector.check(vector.new(1, 2, 3))) + assert.is_true(vector.check(vector.new({x = 3, y = 2, z = 1}))) + + local input = vector.new({ x = 3, y = 2, z = 1 }) + local output = vector.new(input) + assert.same(input, output) + assert.equal(input, output) + assert.is_false(rawequal(input, output)) + assert.equal(input, input:new()) + end) + + it("throws on invalid input", function() + assert.has.errors(function() + vector.new({ x = 3 }) + end) + + assert.has.errors(function() + vector.new({ d = 3 }) + end) + end) + end) + + it("zero()", function() + assert.same({x = 0, y = 0, z = 0}, vector.zero()) + assert.same(vector.new(), vector.zero()) + assert.equal(vector.new(), vector.zero()) + assert.is_true(vector.check(vector.zero())) + end) + + it("copy()", function() + local v = vector.new(1, 2, 3) + assert.same(v, vector.copy(v)) + assert.same(vector.new(v), vector.copy(v)) + assert.equal(vector.new(v), vector.copy(v)) + assert.is_true(vector.check(vector.copy(v))) + end) + + it("indexes", function() + local some_vector = vector.new(24, 42, 13) + assert.equal(24, some_vector[1]) + assert.equal(24, some_vector.x) + assert.equal(42, some_vector[2]) + assert.equal(42, some_vector.y) + assert.equal(13, some_vector[3]) + assert.equal(13, some_vector.z) + + some_vector[1] = 100 + assert.equal(100, some_vector.x) + some_vector.x = 101 + assert.equal(101, some_vector[1]) + + some_vector[2] = 100 + assert.equal(100, some_vector.y) + some_vector.y = 102 + assert.equal(102, some_vector[2]) + + some_vector[3] = 100 + assert.equal(100, some_vector.z) + some_vector.z = 103 + assert.equal(103, some_vector[3]) + end) + + it("direction()", function() + local a = vector.new(1, 0, 0) + local b = vector.new(1, 42, 0) + assert.equal(vector.new(0, 1, 0), vector.direction(a, b)) + assert.equal(vector.new(0, 1, 0), a:direction(b)) + end) + + it("distance()", function() + local a = vector.new(1, 0, 0) + local b = vector.new(3, 42, 9) + assert.is_true(math.abs(43 - vector.distance(a, b)) < 1.0e-12) + assert.is_true(math.abs(43 - a:distance(b)) < 1.0e-12) + assert.equal(0, vector.distance(a, a)) + assert.equal(0, b:distance(b)) + end) + + it("length()", function() + local a = vector.new(0, 0, -23) + assert.equal(0, vector.length(vector.new())) + assert.equal(23, vector.length(a)) + assert.equal(23, a:length()) + end) + + it("normalize()", function() + local a = vector.new(0, 0, -23) + assert.equal(vector.new(0, 0, -1), vector.normalize(a)) + assert.equal(vector.new(0, 0, -1), a:normalize()) + assert.equal(vector.new(), vector.normalize(vector.new())) + end) + + it("floor()", function() + local a = vector.new(0.1, 0.9, -0.5) + assert.equal(vector.new(0, 0, -1), vector.floor(a)) + assert.equal(vector.new(0, 0, -1), a:floor()) + end) + + it("round()", function() + local a = vector.new(0.1, 0.9, -0.5) + assert.equal(vector.new(0, 1, -1), vector.round(a)) + assert.equal(vector.new(0, 1, -1), a:round()) + end) + + it("apply()", function() + local i = 0 + local f = function(x) + i = i + 1 + return x + i + end + local a = vector.new(0.1, 0.9, -0.5) + assert.equal(vector.new(1, 1, 0), vector.apply(a, math.ceil)) + assert.equal(vector.new(1, 1, 0), a:apply(math.ceil)) + assert.equal(vector.new(0.1, 0.9, 0.5), vector.apply(a, math.abs)) + assert.equal(vector.new(0.1, 0.9, 0.5), a:apply(math.abs)) + assert.equal(vector.new(1.1, 2.9, 2.5), vector.apply(a, f)) + assert.equal(vector.new(4.1, 5.9, 5.5), a:apply(f)) + end) + + it("combine()", function() + local a = vector.new(1, 2, 3) + local b = vector.new(3, 2, 1) + assert.equal(vector.add(a, b), vector.combine(a, b, function(x, y) return x + y end)) + assert.equal(vector.new(3, 2, 3), vector.combine(a, b, math.max)) + assert.equal(vector.new(1, 2, 1), vector.combine(a, b, math.min)) + end) + + it("equals()", function() + local function assertE(a, b) + assert.is_true(vector.equals(a, b)) + end + local function assertNE(a, b) + assert.is_false(vector.equals(a, b)) + end + + assertE({x = 0, y = 0, z = 0}, {x = 0, y = 0, z = 0}) + assertE({x = -1, y = 0, z = 1}, {x = -1, y = 0, z = 1}) + assertE({x = -1, y = 0, z = 1}, vector.new(-1, 0, 1)) + local a = {x = 2, y = 4, z = -10} + assertE(a, a) + assertNE({x = -1, y = 0, z = 1}, a) + + assert.equal(vector.new(1, 2, 3), vector.new(1, 2, 3)) + assert.is_true(vector.new(1, 2, 3):equals(vector.new(1, 2, 3))) + assert.not_equal(vector.new(1, 2, 3), vector.new(1, 2, 4)) + assert.is_true(vector.new(1, 2, 3) == vector.new(1, 2, 3)) + assert.is_false(vector.new(1, 2, 3) == vector.new(1, 3, 3)) + end) + + it("metatable is same", function() + local a = vector.new() + local b = vector.new(1, 2, 3) + + assert.equal(true, vector.check(a)) + assert.equal(true, vector.check(b)) + + assert.equal(vector.metatable, getmetatable(a)) + assert.equal(vector.metatable, getmetatable(b)) + assert.equal(vector.metatable, a.metatable) + end) + + it("sort()", function() + local a = vector.new(1, 2, 3) + local b = vector.new(0.5, 232, -2) + local sorted = {vector.new(0.5, 2, -2), vector.new(1, 232, 3)} + assert.same(sorted, {vector.sort(a, b)}) + assert.same(sorted, {a:sort(b)}) + end) + + it("angle()", function() + assert.equal(math.pi, vector.angle(vector.new(-1, -2, -3), vector.new(1, 2, 3))) + assert.equal(math.pi/2, vector.new(0, 1, 0):angle(vector.new(1, 0, 0))) + end) + + it("dot()", function() + assert.equal(-14, vector.dot(vector.new(-1, -2, -3), vector.new(1, 2, 3))) + assert.equal(0, vector.new():dot(vector.new(1, 2, 3))) + end) + + it("cross()", function() + local a = vector.new(-1, -2, 0) + local b = vector.new(1, 2, 3) + assert.equal(vector.new(-6, 3, 0), vector.cross(a, b)) + assert.equal(vector.new(-6, 3, 0), a:cross(b)) + end) + + it("offset()", function() + assert.same({x = 41, y = 52, z = 63}, vector.offset(vector.new(1, 2, 3), 40, 50, 60)) + assert.equal(vector.new(41, 52, 63), vector.offset(vector.new(1, 2, 3), 40, 50, 60)) + assert.equal(vector.new(41, 52, 63), vector.new(1, 2, 3):offset(40, 50, 60)) + end) + + it("is()", function() + local some_table1 = {foo = 13, [42] = 1, "bar", 2} + local some_table2 = {1, 2, 3} + local some_table3 = {x = 1, 2, 3} + local some_table4 = {1, 2, z = 3} + local old = {x = 1, y = 2, z = 3} + local real = vector.new(1, 2, 3) + + assert.is_false(vector.check(nil)) + assert.is_false(vector.check(1)) + assert.is_false(vector.check(true)) + assert.is_false(vector.check("foo")) + assert.is_false(vector.check(some_table1)) + assert.is_false(vector.check(some_table2)) + assert.is_false(vector.check(some_table3)) + assert.is_false(vector.check(some_table4)) + assert.is_false(vector.check(old)) + assert.is_true(vector.check(real)) + assert.is_true(real:check()) + end) + + it("global pairs", function() + local out = {} + local vec = vector.new(10, 20, 30) + for k, v in pairs(vec) do + out[k] = v + end + assert.same({x = 10, y = 20, z = 30}, out) + end) + + it("abusing works", function() + local v = vector.new(1, 2, 3) + v.a = 1 + assert.equal(1, v.a) + + local a_is_there = false + for key, value in pairs(v) do + if key == "a" then + a_is_there = true + assert.equal(value, 1) + break + end + end + assert.is_true(a_is_there) + end) + + it("add()", function() + local a = vector.new(1, 2, 3) + local b = vector.new(1, 4, 3) + local c = vector.new(2, 6, 6) + assert.equal(c, vector.add(a, {x = 1, y = 4, z = 3})) + assert.equal(c, vector.add(a, b)) + assert.equal(c, a:add(b)) + assert.equal(c, a + b) + assert.equal(c, b + a) + end) + + it("subtract()", function() + local a = vector.new(1, 2, 3) + local b = vector.new(2, 4, 3) + local c = vector.new(-1, -2, 0) + assert.equal(c, vector.subtract(a, {x = 2, y = 4, z = 3})) + assert.equal(c, vector.subtract(a, b)) + assert.equal(c, a:subtract(b)) + assert.equal(c, a - b) + assert.equal(c, -b + a) + end) + + it("multiply()", function() + local a = vector.new(1, 2, 3) + local b = vector.new(2, 4, 3) + local c = vector.new(2, 8, 9) + local s = 2 + local d = vector.new(2, 4, 6) + assert.equal(c, vector.multiply(a, {x = 2, y = 4, z = 3})) + assert.equal(c, vector.multiply(a, b)) + assert.equal(d, vector.multiply(a, s)) + assert.equal(d, a:multiply(s)) + assert.equal(d, a * s) + assert.equal(d, s * a) + assert.equal(-a, -1 * a) + end) + + it("divide()", function() + local a = vector.new(1, 2, 3) + local b = vector.new(2, 4, 3) + local c = vector.new(0.5, 0.5, 1) + local s = 2 + local d = vector.new(0.5, 1, 1.5) + assert.equal(c, vector.divide(a, {x = 2, y = 4, z = 3})) + assert.equal(c, vector.divide(a, b)) + assert.equal(d, vector.divide(a, s)) + assert.equal(d, a:divide(s)) + assert.equal(d, a / s) + assert.equal(d, 1/s * a) + assert.equal(-a, a / -1) + end) + + it("to_string()", function() + local v = vector.new(1, 2, 3.14) + assert.same("(1, 2, 3.14)", vector.to_string(v)) + assert.same("(1, 2, 3.14)", v:to_string()) + assert.same("(1, 2, 3.14)", tostring(v)) + end) + + it("from_string()", function() + local v = vector.new(1, 2, 3.14) + assert.is_true(vector.check(vector.from_string("(1, 2, 3.14)"))) + assert.same({v, 13}, {vector.from_string("(1, 2, 3.14)")}) + assert.same({v, 12}, {vector.from_string("(1,2 ,3.14)")}) + assert.same({v, 12}, {vector.from_string("(1,2,3.14,)")}) + assert.same({v, 11}, {vector.from_string("(1 2 3.14)")}) + assert.same({v, 15}, {vector.from_string("( 1, 2, 3.14 )")}) + assert.same({v, 15}, {vector.from_string(" ( 1, 2, 3.14) ")}) + assert.same({vector.new(), 8}, {vector.from_string("(0,0,0) ( 1, 2, 3.14) ")}) + assert.same({v, 22}, {vector.from_string("(0,0,0) ( 1, 2, 3.14) ", 8)}) + assert.same({v, 22}, {vector.from_string("(0,0,0) ( 1, 2, 3.14) ", 9)}) + assert.same(nil, vector.from_string("nothing")) + end) + + -- This function is needed because of floating point imprecision. + local function almost_equal(a, b) + if type(a) == "number" then + return math.abs(a - b) < 0.00000000001 + end + return vector.distance(a, b) < 0.000000000001 + end + + describe("rotate_around_axis()", function() + it("rotates", function() + assert.True(almost_equal({x = -1, y = 0, z = 0}, + vector.rotate_around_axis({x = 1, y = 0, z = 0}, {x = 0, y = 1, z = 0}, math.pi))) + assert.True(almost_equal({x = 0, y = 1, z = 0}, + vector.rotate_around_axis({x = 0, y = 0, z = 1}, {x = 1, y = 0, z = 0}, math.pi / 2))) + assert.True(almost_equal({x = 4, y = 1, z = 1}, + vector.rotate_around_axis({x = 4, y = 1, z = 1}, {x = 4, y = 1, z = 1}, math.pi / 6))) + end) + it("keeps distance to axis", function() + local rotate1 = {x = 1, y = 3, z = 1} + local axis1 = {x = 1, y = 3, z = 2} + local rotated1 = vector.rotate_around_axis(rotate1, axis1, math.pi / 13) + assert.True(almost_equal(vector.distance(axis1, rotate1), vector.distance(axis1, rotated1))) + local rotate2 = {x = 1, y = 1, z = 3} + local axis2 = {x = 2, y = 6, z = 100} + local rotated2 = vector.rotate_around_axis(rotate2, axis2, math.pi / 23) + assert.True(almost_equal(vector.distance(axis2, rotate2), vector.distance(axis2, rotated2))) + local rotate3 = {x = 1, y = -1, z = 3} + local axis3 = {x = 2, y = 6, z = 100} + local rotated3 = vector.rotate_around_axis(rotate3, axis3, math.pi / 2) + assert.True(almost_equal(vector.distance(axis3, rotate3), vector.distance(axis3, rotated3))) + end) + it("rotates back", function() + local rotate1 = {x = 1, y = 3, z = 1} + local axis1 = {x = 1, y = 3, z = 2} + local rotated1 = vector.rotate_around_axis(rotate1, axis1, math.pi / 13) + rotated1 = vector.rotate_around_axis(rotated1, axis1, -math.pi / 13) + assert.True(almost_equal(rotate1, rotated1)) + local rotate2 = {x = 1, y = 1, z = 3} + local axis2 = {x = 2, y = 6, z = 100} + local rotated2 = vector.rotate_around_axis(rotate2, axis2, math.pi / 23) + rotated2 = vector.rotate_around_axis(rotated2, axis2, -math.pi / 23) + assert.True(almost_equal(rotate2, rotated2)) + local rotate3 = {x = 1, y = -1, z = 3} + local axis3 = {x = 2, y = 6, z = 100} + local rotated3 = vector.rotate_around_axis(rotate3, axis3, math.pi / 2) + rotated3 = vector.rotate_around_axis(rotated3, axis3, -math.pi / 2) + assert.True(almost_equal(rotate3, rotated3)) + end) + it("is right handed", function() + local v_before1 = {x = 0, y = 1, z = -1} + local v_after1 = vector.rotate_around_axis(v_before1, {x = 1, y = 0, z = 0}, math.pi / 4) + assert.True(almost_equal(vector.normalize(vector.cross(v_after1, v_before1)), {x = 1, y = 0, z = 0})) + + local v_before2 = {x = 0, y = 3, z = 4} + local v_after2 = vector.rotate_around_axis(v_before2, {x = 1, y = 0, z = 0}, 2 * math.pi / 5) + assert.True(almost_equal(vector.normalize(vector.cross(v_after2, v_before2)), {x = 1, y = 0, z = 0})) + + local v_before3 = {x = 1, y = 0, z = -1} + local v_after3 = vector.rotate_around_axis(v_before3, {x = 0, y = 1, z = 0}, math.pi / 4) + assert.True(almost_equal(vector.normalize(vector.cross(v_after3, v_before3)), {x = 0, y = 1, z = 0})) + + local v_before4 = {x = 3, y = 0, z = 4} + local v_after4 = vector.rotate_around_axis(v_before4, {x = 0, y = 1, z = 0}, 2 * math.pi / 5) + assert.True(almost_equal(vector.normalize(vector.cross(v_after4, v_before4)), {x = 0, y = 1, z = 0})) + + local v_before5 = {x = 1, y = -1, z = 0} + local v_after5 = vector.rotate_around_axis(v_before5, {x = 0, y = 0, z = 1}, math.pi / 4) + assert.True(almost_equal(vector.normalize(vector.cross(v_after5, v_before5)), {x = 0, y = 0, z = 1})) + + local v_before6 = {x = 3, y = 4, z = 0} + local v_after6 = vector.rotate_around_axis(v_before6, {x = 0, y = 0, z = 1}, 2 * math.pi / 5) + assert.True(almost_equal(vector.normalize(vector.cross(v_after6, v_before6)), {x = 0, y = 0, z = 1})) + end) + end) + + describe("rotate()", function() + it("rotates", function() + assert.True(almost_equal({x = -1, y = 0, z = 0}, + vector.rotate({x = 1, y = 0, z = 0}, {x = 0, y = math.pi, z = 0}))) + assert.True(almost_equal({x = 0, y = -1, z = 0}, + vector.rotate({x = 1, y = 0, z = 0}, {x = 0, y = 0, z = math.pi / 2}))) + assert.True(almost_equal({x = 1, y = 0, z = 0}, + vector.rotate({x = 1, y = 0, z = 0}, {x = math.pi / 123, y = 0, z = 0}))) + end) + it("is counterclockwise", function() + local v_before1 = {x = 0, y = 1, z = -1} + local v_after1 = vector.rotate(v_before1, {x = math.pi / 4, y = 0, z = 0}) + assert.True(almost_equal(vector.normalize(vector.cross(v_after1, v_before1)), {x = 1, y = 0, z = 0})) + + local v_before2 = {x = 0, y = 3, z = 4} + local v_after2 = vector.rotate(v_before2, {x = 2 * math.pi / 5, y = 0, z = 0}) + assert.True(almost_equal(vector.normalize(vector.cross(v_after2, v_before2)), {x = 1, y = 0, z = 0})) + + local v_before3 = {x = 1, y = 0, z = -1} + local v_after3 = vector.rotate(v_before3, {x = 0, y = math.pi / 4, z = 0}) + assert.True(almost_equal(vector.normalize(vector.cross(v_after3, v_before3)), {x = 0, y = 1, z = 0})) + + local v_before4 = {x = 3, y = 0, z = 4} + local v_after4 = vector.rotate(v_before4, {x = 0, y = 2 * math.pi / 5, z = 0}) + assert.True(almost_equal(vector.normalize(vector.cross(v_after4, v_before4)), {x = 0, y = 1, z = 0})) + + local v_before5 = {x = 1, y = -1, z = 0} + local v_after5 = vector.rotate(v_before5, {x = 0, y = 0, z = math.pi / 4}) + assert.True(almost_equal(vector.normalize(vector.cross(v_after5, v_before5)), {x = 0, y = 0, z = 1})) + + local v_before6 = {x = 3, y = 4, z = 0} + local v_after6 = vector.rotate(v_before6, {x = 0, y = 0, z = 2 * math.pi / 5}) + assert.True(almost_equal(vector.normalize(vector.cross(v_after6, v_before6)), {x = 0, y = 0, z = 1})) + end) + end) + + it("dir_to_rotation()", function() + -- Comparing rotations (pitch, yaw, roll) is hard because of certain ambiguities, + -- e.g. (pi, 0, pi) looks exactly the same as (0, pi, 0) + -- So instead we convert the rotation back to vectors and compare these. + local function forward_at_rot(rot) + return vector.rotate(vector.new(0, 0, 1), rot) + end + local function up_at_rot(rot) + return vector.rotate(vector.new(0, 1, 0), rot) + end + local rot1 = vector.dir_to_rotation({x = 1, y = 0, z = 0}, {x = 0, y = 1, z = 0}) + assert.True(almost_equal({x = 1, y = 0, z = 0}, forward_at_rot(rot1))) + assert.True(almost_equal({x = 0, y = 1, z = 0}, up_at_rot(rot1))) + local rot2 = vector.dir_to_rotation({x = 1, y = 1, z = 0}, {x = 0, y = 0, z = 1}) + assert.True(almost_equal({x = 1/math.sqrt(2), y = 1/math.sqrt(2), z = 0}, forward_at_rot(rot2))) + assert.True(almost_equal({x = 0, y = 0, z = 1}, up_at_rot(rot2))) + for i = 1, 1000 do + local rand_vec = vector.new(math.random(), math.random(), math.random()) + if vector.length(rand_vec) ~= 0 then + local rot_1 = vector.dir_to_rotation(rand_vec) + local rot_2 = { + x = math.atan2(rand_vec.y, math.sqrt(rand_vec.z * rand_vec.z + rand_vec.x * rand_vec.x)), + y = -math.atan2(rand_vec.x, rand_vec.z), + z = 0 + } + assert.True(almost_equal(rot_1, rot_2)) + end + end + + end) +end) diff --git a/builtin/common/vector.lua b/builtin/common/vector.lua new file mode 100644 index 0000000..a08472e --- /dev/null +++ b/builtin/common/vector.lua @@ -0,0 +1,368 @@ +--[[ +Vector helpers +Note: The vector.*-functions must be able to accept old vectors that had no metatables +]] + +-- localize functions +local setmetatable = setmetatable + +-- vector.metatable is set by C++. +local metatable = vector.metatable + +local xyz = {"x", "y", "z"} + +-- only called when rawget(v, key) returns nil +function metatable.__index(v, key) + return rawget(v, xyz[key]) or vector[key] +end + +-- only called when rawget(v, key) returns nil +function metatable.__newindex(v, key, value) + rawset(v, xyz[key] or key, value) +end + +-- constructors + +local function fast_new(x, y, z) + return setmetatable({x = x, y = y, z = z}, metatable) +end + +function vector.new(a, b, c) + if a and b and c then + return fast_new(a, b, c) + end + + -- deprecated, use vector.copy and vector.zero directly + if type(a) == "table" then + return vector.copy(a) + else + assert(not a, "Invalid arguments for vector.new()") + return vector.zero() + end +end + +function vector.zero() + return fast_new(0, 0, 0) +end + +function vector.copy(v) + assert(v.x and v.y and v.z, "Invalid vector passed to vector.copy()") + return fast_new(v.x, v.y, v.z) +end + +function vector.from_string(s, init) + local x, y, z, np = string.match(s, "^%s*%(%s*([^%s,]+)%s*[,%s]%s*([^%s,]+)%s*[,%s]" .. + "%s*([^%s,]+)%s*[,%s]?%s*%)()", init) + x = tonumber(x) + y = tonumber(y) + z = tonumber(z) + if not (x and y and z) then + return nil + end + return fast_new(x, y, z), np +end + +function vector.to_string(v) + return string.format("(%g, %g, %g)", v.x, v.y, v.z) +end +metatable.__tostring = vector.to_string + +function vector.equals(a, b) + return a.x == b.x and + a.y == b.y and + a.z == b.z +end +metatable.__eq = vector.equals + +-- unary operations + +function vector.length(v) + return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z) +end +-- Note: we can not use __len because it is already used for primitive table length + +function vector.normalize(v) + local len = vector.length(v) + if len == 0 then + return fast_new(0, 0, 0) + else + return vector.divide(v, len) + end +end + +function vector.floor(v) + return vector.apply(v, math.floor) +end + +function vector.round(v) + return fast_new( + math.round(v.x), + math.round(v.y), + math.round(v.z) + ) +end + +function vector.apply(v, func) + return fast_new( + func(v.x), + func(v.y), + func(v.z) + ) +end + +function vector.combine(a, b, func) + return fast_new( + func(a.x, b.x), + func(a.y, b.y), + func(a.z, b.z) + ) +end + +function vector.distance(a, b) + local x = a.x - b.x + local y = a.y - b.y + local z = a.z - b.z + return math.sqrt(x * x + y * y + z * z) +end + +function vector.direction(pos1, pos2) + return vector.subtract(pos2, pos1):normalize() +end + +function vector.angle(a, b) + local dotp = vector.dot(a, b) + local cp = vector.cross(a, b) + local crossplen = vector.length(cp) + return math.atan2(crossplen, dotp) +end + +function vector.dot(a, b) + return a.x * b.x + a.y * b.y + a.z * b.z +end + +function vector.cross(a, b) + return fast_new( + a.y * b.z - a.z * b.y, + a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x + ) +end + +function metatable.__unm(v) + return fast_new(-v.x, -v.y, -v.z) +end + +-- add, sub, mul, div operations + +function vector.add(a, b) + if type(b) == "table" then + return fast_new( + a.x + b.x, + a.y + b.y, + a.z + b.z + ) + else + return fast_new( + a.x + b, + a.y + b, + a.z + b + ) + end +end +function metatable.__add(a, b) + return fast_new( + a.x + b.x, + a.y + b.y, + a.z + b.z + ) +end + +function vector.subtract(a, b) + if type(b) == "table" then + return fast_new( + a.x - b.x, + a.y - b.y, + a.z - b.z + ) + else + return fast_new( + a.x - b, + a.y - b, + a.z - b + ) + end +end +function metatable.__sub(a, b) + return fast_new( + a.x - b.x, + a.y - b.y, + a.z - b.z + ) +end + +function vector.multiply(a, b) + if type(b) == "table" then + return fast_new( + a.x * b.x, + a.y * b.y, + a.z * b.z + ) + else + return fast_new( + a.x * b, + a.y * b, + a.z * b + ) + end +end +function metatable.__mul(a, b) + if type(a) == "table" then + return fast_new( + a.x * b, + a.y * b, + a.z * b + ) + else + return fast_new( + a * b.x, + a * b.y, + a * b.z + ) + end +end + +function vector.divide(a, b) + if type(b) == "table" then + return fast_new( + a.x / b.x, + a.y / b.y, + a.z / b.z + ) + else + return fast_new( + a.x / b, + a.y / b, + a.z / b + ) + end +end +function metatable.__div(a, b) + -- scalar/vector makes no sense + return fast_new( + a.x / b, + a.y / b, + a.z / b + ) +end + +-- misc stuff + +function vector.offset(v, x, y, z) + return fast_new( + v.x + x, + v.y + y, + v.z + z + ) +end + +function vector.sort(a, b) + return fast_new(math.min(a.x, b.x), math.min(a.y, b.y), math.min(a.z, b.z)), + fast_new(math.max(a.x, b.x), math.max(a.y, b.y), math.max(a.z, b.z)) +end + +function vector.check(v) + return getmetatable(v) == metatable +end + +local function sin(x) + if x % math.pi == 0 then + return 0 + else + return math.sin(x) + end +end + +local function cos(x) + if x % math.pi == math.pi / 2 then + return 0 + else + return math.cos(x) + end +end + +function vector.rotate_around_axis(v, axis, angle) + local cosangle = cos(angle) + local sinangle = sin(angle) + axis = vector.normalize(axis) + -- https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula + local dot_axis = vector.multiply(axis, vector.dot(axis, v)) + local cross = vector.cross(v, axis) + return vector.new( + cross.x * sinangle + (v.x - dot_axis.x) * cosangle + dot_axis.x, + cross.y * sinangle + (v.y - dot_axis.y) * cosangle + dot_axis.y, + cross.z * sinangle + (v.z - dot_axis.z) * cosangle + dot_axis.z + ) +end + +function vector.rotate(v, rot) + local sinpitch = sin(-rot.x) + local sinyaw = sin(-rot.y) + local sinroll = sin(-rot.z) + local cospitch = cos(rot.x) + local cosyaw = cos(rot.y) + local cosroll = math.cos(rot.z) + -- Rotation matrix that applies yaw, pitch and roll + local matrix = { + { + sinyaw * sinpitch * sinroll + cosyaw * cosroll, + sinyaw * sinpitch * cosroll - cosyaw * sinroll, + sinyaw * cospitch, + }, + { + cospitch * sinroll, + cospitch * cosroll, + -sinpitch, + }, + { + cosyaw * sinpitch * sinroll - sinyaw * cosroll, + cosyaw * sinpitch * cosroll + sinyaw * sinroll, + cosyaw * cospitch, + }, + } + -- Compute matrix multiplication: `matrix` * `v` + return vector.new( + matrix[1][1] * v.x + matrix[1][2] * v.y + matrix[1][3] * v.z, + matrix[2][1] * v.x + matrix[2][2] * v.y + matrix[2][3] * v.z, + matrix[3][1] * v.x + matrix[3][2] * v.y + matrix[3][3] * v.z + ) +end + +function vector.dir_to_rotation(forward, up) + forward = vector.normalize(forward) + local rot = vector.new(math.asin(forward.y), -math.atan2(forward.x, forward.z), 0) + if not up then + return rot + end + assert(vector.dot(forward, up) < 0.000001, + "Invalid vectors passed to vector.dir_to_rotation().") + up = vector.normalize(up) + -- Calculate vector pointing up with roll = 0, just based on forward vector. + local forwup = vector.rotate(vector.new(0, 1, 0), rot) + -- 'forwup' and 'up' are now in a plane with 'forward' as normal. + -- The angle between them is the absolute of the roll value we're looking for. + rot.z = vector.angle(forwup, up) + + -- Since vector.angle never returns a negative value or a value greater + -- than math.pi, rot.z has to be inverted sometimes. + -- To determine wether this is the case, we rotate the up vector back around + -- the forward vector and check if it worked out. + local back = vector.rotate_around_axis(up, forward, -rot.z) + + -- We don't use vector.equals for this because of floating point imprecision. + if (back.x - forwup.x) * (back.x - forwup.x) + + (back.y - forwup.y) * (back.y - forwup.y) + + (back.z - forwup.z) * (back.z - forwup.z) > 0.0000001 then + rot.z = -rot.z + end + return rot +end diff --git a/builtin/fstk/buttonbar.lua b/builtin/fstk/buttonbar.lua new file mode 100644 index 0000000..4655883 --- /dev/null +++ b/builtin/fstk/buttonbar.lua @@ -0,0 +1,215 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +local function buttonbar_formspec(self) + + if self.hidden then + return "" + end + + local formspec = string.format("box[%f,%f;%f,%f;%s]", + self.pos.x,self.pos.y ,self.size.x,self.size.y,self.bgcolor) + + for i=self.startbutton,#self.buttons,1 do + local btn_name = self.buttons[i].name + local btn_pos = {} + + if self.orientation == "horizontal" then + btn_pos.x = self.pos.x + --base pos + (i - self.startbutton) * self.btn_size + --button offset + self.btn_initial_offset + else + btn_pos.x = self.pos.x + (self.btn_size * 0.05) + end + + if self.orientation == "vertical" then + btn_pos.y = self.pos.y + --base pos + (i - self.startbutton) * self.btn_size + --button offset + self.btn_initial_offset + else + btn_pos.y = self.pos.y + (self.btn_size * 0.05) + end + + if (self.orientation == "vertical" and + (btn_pos.y + self.btn_size <= self.pos.y + self.size.y)) or + (self.orientation == "horizontal" and + (btn_pos.x + self.btn_size <= self.pos.x + self.size.x)) then + + local borders="true" + + if self.buttons[i].image ~= nil then + borders="false" + end + + formspec = formspec .. + string.format("image_button[%f,%f;%f,%f;%s;%s;%s;true;%s]tooltip[%s;%s]", + btn_pos.x, btn_pos.y, self.btn_size, self.btn_size, + self.buttons[i].image, btn_name, self.buttons[i].caption, + borders, btn_name, self.buttons[i].tooltip) + else + --print("end of displayable buttons: orientation: " .. self.orientation) + --print( "button_end: " .. (btn_pos.y + self.btn_size - (self.btn_size * 0.05))) + --print( "bar_end: " .. (self.pos.x + self.size.x)) + break + end + end + + if (self.have_move_buttons) then + local btn_dec_pos = {} + btn_dec_pos.x = self.pos.x + (self.btn_size * 0.05) + btn_dec_pos.y = self.pos.y + (self.btn_size * 0.05) + local btn_inc_pos = {} + local btn_size = {} + + if self.orientation == "horizontal" then + btn_size.x = 0.5 + btn_size.y = self.btn_size + btn_inc_pos.x = self.pos.x + self.size.x - 0.5 + btn_inc_pos.y = self.pos.y + (self.btn_size * 0.05) + else + btn_size.x = self.btn_size + btn_size.y = 0.5 + btn_inc_pos.x = self.pos.x + (self.btn_size * 0.05) + btn_inc_pos.y = self.pos.y + self.size.y - 0.5 + end + + local text_dec = "<" + local text_inc = ">" + if self.orientation == "vertical" then + text_dec = "^" + text_inc = "v" + end + + formspec = formspec .. + string.format("image_button[%f,%f;%f,%f;;btnbar_dec_%s;%s;true;true]", + btn_dec_pos.x, btn_dec_pos.y, btn_size.x, btn_size.y, + self.name, text_dec) + + formspec = formspec .. + string.format("image_button[%f,%f;%f,%f;;btnbar_inc_%s;%s;true;true]", + btn_inc_pos.x, btn_inc_pos.y, btn_size.x, btn_size.y, + self.name, text_inc) + end + + return formspec +end + +local function buttonbar_buttonhandler(self, fields) + + if fields["btnbar_inc_" .. self.name] ~= nil and + self.startbutton < #self.buttons then + + self.startbutton = self.startbutton + 1 + return true + end + + if fields["btnbar_dec_" .. self.name] ~= nil and self.startbutton > 1 then + self.startbutton = self.startbutton - 1 + return true + end + + for i=1,#self.buttons,1 do + if fields[self.buttons[i].name] ~= nil then + return self.userbuttonhandler(fields) + end + end +end + +local buttonbar_metatable = { + handle_buttons = buttonbar_buttonhandler, + handle_events = function(self, event) end, + get_formspec = buttonbar_formspec, + + hide = function(self) self.hidden = true end, + show = function(self) self.hidden = false end, + + delete = function(self) ui.delete(self) end, + + add_button = function(self, name, caption, image, tooltip) + if caption == nil then caption = "" end + if image == nil then image = "" end + if tooltip == nil then tooltip = "" end + + self.buttons[#self.buttons + 1] = { + name = name, + caption = caption, + image = image, + tooltip = tooltip + } + if self.orientation == "horizontal" then + if ( (self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2) + > self.size.x ) then + + self.btn_initial_offset = self.btn_size * 0.05 + 0.5 + self.have_move_buttons = true + end + else + if ((self.btn_size * #self.buttons) + (self.btn_size * 0.05 *2) + > self.size.y ) then + + self.btn_initial_offset = self.btn_size * 0.05 + 0.5 + self.have_move_buttons = true + end + end + end, + + set_bgparams = function(self, bgcolor) + if (type(bgcolor) == "string") then + self.bgcolor = bgcolor + end + end, +} + +buttonbar_metatable.__index = buttonbar_metatable + +function buttonbar_create(name, cbf_buttonhandler, pos, orientation, size) + assert(name ~= nil) + assert(cbf_buttonhandler ~= nil) + assert(orientation == "vertical" or orientation == "horizontal") + assert(pos ~= nil and type(pos) == "table") + assert(size ~= nil and type(size) == "table") + + local self = {} + self.name = name + self.type = "addon" + self.bgcolor = "#000000" + self.pos = pos + self.size = size + self.orientation = orientation + self.startbutton = 1 + self.have_move_buttons = false + self.hidden = false + + if self.orientation == "horizontal" then + self.btn_size = self.size.y + else + self.btn_size = self.size.x + end + + if (self.btn_initial_offset == nil) then + self.btn_initial_offset = self.btn_size * 0.05 + end + + self.userbuttonhandler = cbf_buttonhandler + self.buttons = {} + + setmetatable(self,buttonbar_metatable) + + ui.add(self) + return self +end diff --git a/builtin/fstk/dialog.lua b/builtin/fstk/dialog.lua new file mode 100644 index 0000000..ea57df1 --- /dev/null +++ b/builtin/fstk/dialog.lua @@ -0,0 +1,88 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--this program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local function dialog_event_handler(self,event) + if self.user_eventhandler == nil or + self.user_eventhandler(event) == false then + + --close dialog on esc + if event == "MenuQuit" then + self:delete() + return true + end + end +end + +local dialog_metatable = { + eventhandler = dialog_event_handler, + get_formspec = function(self) + if not self.hidden then return self.formspec(self.data) end + end, + handle_buttons = function(self,fields) + if not self.hidden then return self.buttonhandler(self,fields) end + end, + handle_events = function(self,event) + if not self.hidden then return self.eventhandler(self,event) end + end, + hide = function(self) self.hidden = true end, + show = function(self) self.hidden = false end, + delete = function(self) + if self.parent ~= nil then + self.parent:show() + end + ui.delete(self) + end, + set_parent = function(self,parent) self.parent = parent end +} +dialog_metatable.__index = dialog_metatable + +function dialog_create(name,get_formspec,buttonhandler,eventhandler) + local self = {} + + self.name = name + self.type = "toplevel" + self.hidden = true + self.data = {} + + self.formspec = get_formspec + self.buttonhandler = buttonhandler + self.user_eventhandler = eventhandler + + setmetatable(self,dialog_metatable) + + ui.add(self) + return self +end + +function messagebox(name, message) + return dialog_create(name, + function() + return ([[ + formspec_version[3] + size[8,3] + textarea[0.375,0.375;7.25,1.2;;;%s] + button[3,1.825;2,0.8;ok;%s] + ]]):format(message, fgettext("OK")) + end, + function(this, fields) + if fields.ok then + this:delete() + return true + end + end, + nil) +end diff --git a/builtin/fstk/tabview.lua b/builtin/fstk/tabview.lua new file mode 100644 index 0000000..424d329 --- /dev/null +++ b/builtin/fstk/tabview.lua @@ -0,0 +1,257 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +-------------------------------------------------------------------------------- +-- A tabview implementation -- +-- Usage: -- +-- tabview.create: returns initialized tabview raw element -- +-- element.add(tab): add a tab declaration -- +-- element.handle_buttons() -- +-- element.handle_events() -- +-- element.getFormspec() returns formspec of tabview -- +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +local function add_tab(self,tab) + assert(tab.size == nil or (type(tab.size) == table and + tab.size.x ~= nil and tab.size.y ~= nil)) + assert(tab.cbf_formspec ~= nil and type(tab.cbf_formspec) == "function") + assert(tab.cbf_button_handler == nil or + type(tab.cbf_button_handler) == "function") + assert(tab.cbf_events == nil or type(tab.cbf_events) == "function") + + local newtab = { + name = tab.name, + caption = tab.caption, + button_handler = tab.cbf_button_handler, + event_handler = tab.cbf_events, + get_formspec = tab.cbf_formspec, + tabsize = tab.tabsize, + on_change = tab.on_change, + tabdata = {}, + } + + self.tablist[#self.tablist + 1] = newtab + + if self.last_tab_index == #self.tablist then + self.current_tab = tab.name + if tab.on_activate ~= nil then + tab.on_activate(nil,tab.name) + end + end +end + +-------------------------------------------------------------------------------- +local function get_formspec(self) + if self.hidden or (self.parent ~= nil and self.parent.hidden) then + return "" + end + local tab = self.tablist[self.last_tab_index] + + local content, prepend = tab.get_formspec(self, tab.name, tab.tabdata, tab.tabsize) + + if self.parent == nil and not prepend then + local tsize = tab.tabsize or {width=self.width, height=self.height} + prepend = string.format("size[%f,%f,%s]", tsize.width, tsize.height, + dump(self.fixed_size)) + end + + local formspec = (prepend or "") .. self:tab_header() .. content + return formspec +end + +-------------------------------------------------------------------------------- +local function handle_buttons(self,fields) + + if self.hidden then + return false + end + + if self:handle_tab_buttons(fields) then + return true + end + + if self.glb_btn_handler ~= nil and + self.glb_btn_handler(self,fields) then + return true + end + + local tab = self.tablist[self.last_tab_index] + if tab.button_handler ~= nil then + return tab.button_handler(self, fields, tab.name, tab.tabdata) + end + + return false +end + +-------------------------------------------------------------------------------- +local function handle_events(self,event) + + if self.hidden then + return false + end + + if self.glb_evt_handler ~= nil and + self.glb_evt_handler(self,event) then + return true + end + + local tab = self.tablist[self.last_tab_index] + if tab.evt_handler ~= nil then + return tab.evt_handler(self, event, tab.name, tab.tabdata) + end + + return false +end + + +-------------------------------------------------------------------------------- +local function tab_header(self) + + local toadd = "" + + for i=1,#self.tablist,1 do + + if toadd ~= "" then + toadd = toadd .. "," + end + + toadd = toadd .. self.tablist[i].caption + end + return string.format("tabheader[%f,%f;%s;%s;%i;true;false]", + self.header_x, self.header_y, self.name, toadd, self.last_tab_index); +end + +-------------------------------------------------------------------------------- +local function switch_to_tab(self, index) + --first call on_change for tab to leave + if self.tablist[self.last_tab_index].on_change ~= nil then + self.tablist[self.last_tab_index].on_change("LEAVE", + self.current_tab, self.tablist[index].name) + end + + --update tabview data + self.last_tab_index = index + local old_tab = self.current_tab + self.current_tab = self.tablist[index].name + + if (self.autosave_tab) then + core.settings:set(self.name .. "_LAST",self.current_tab) + end + + -- call for tab to enter + if self.tablist[index].on_change ~= nil then + self.tablist[index].on_change("ENTER", + old_tab,self.current_tab) + end +end + +-------------------------------------------------------------------------------- +local function handle_tab_buttons(self,fields) + --save tab selection to config file + if fields[self.name] then + local index = tonumber(fields[self.name]) + switch_to_tab(self, index) + return true + end + + return false +end + +-------------------------------------------------------------------------------- +local function set_tab_by_name(self, name) + for i=1,#self.tablist,1 do + if self.tablist[i].name == name then + switch_to_tab(self, i) + return true + end + end + + return false +end + +-------------------------------------------------------------------------------- +local function hide_tabview(self) + self.hidden=true + + --call on_change as we're not gonna show self tab any longer + if self.tablist[self.last_tab_index].on_change ~= nil then + self.tablist[self.last_tab_index].on_change("LEAVE", + self.current_tab, nil) + end +end + +-------------------------------------------------------------------------------- +local function show_tabview(self) + self.hidden=false + + -- call for tab to enter + if self.tablist[self.last_tab_index].on_change ~= nil then + self.tablist[self.last_tab_index].on_change("ENTER", + nil,self.current_tab) + end +end + +local tabview_metatable = { + add = add_tab, + handle_buttons = handle_buttons, + handle_events = handle_events, + get_formspec = get_formspec, + show = show_tabview, + hide = hide_tabview, + delete = function(self) ui.delete(self) end, + set_parent = function(self,parent) self.parent = parent end, + set_autosave_tab = + function(self,value) self.autosave_tab = value end, + set_tab = set_tab_by_name, + set_global_button_handler = + function(self,handler) self.glb_btn_handler = handler end, + set_global_event_handler = + function(self,handler) self.glb_evt_handler = handler end, + set_fixed_size = + function(self,state) self.fixed_size = state end, + tab_header = tab_header, + handle_tab_buttons = handle_tab_buttons +} + +tabview_metatable.__index = tabview_metatable + +-------------------------------------------------------------------------------- +function tabview_create(name, size, tabheaderpos) + local self = {} + + self.name = name + self.type = "toplevel" + self.width = size.x + self.height = size.y + self.header_x = tabheaderpos.x + self.header_y = tabheaderpos.y + + setmetatable(self, tabview_metatable) + + self.fixed_size = true + self.hidden = true + self.current_tab = nil + self.last_tab_index = 1 + self.tablist = {} + + self.autosave_tab = false + + ui.add(self) + return self +end diff --git a/builtin/fstk/ui.lua b/builtin/fstk/ui.lua new file mode 100644 index 0000000..13f9cbe --- /dev/null +++ b/builtin/fstk/ui.lua @@ -0,0 +1,212 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +ui = {} +ui.childlist = {} +ui.default = nil +-- Whether fstk is currently showing its own formspec instead of active ui elements. +ui.overridden = false + +-------------------------------------------------------------------------------- +function ui.add(child) + --TODO check child + ui.childlist[child.name] = child + + return child.name +end + +-------------------------------------------------------------------------------- +function ui.delete(child) + + if ui.childlist[child.name] == nil then + return false + end + + ui.childlist[child.name] = nil + return true +end + +-------------------------------------------------------------------------------- +function ui.set_default(name) + ui.default = name +end + +-------------------------------------------------------------------------------- +function ui.find_by_name(name) + return ui.childlist[name] +end + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-- Internal functions not to be called from user +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- + +function ui.update() + ui.overridden = false + local formspec = {} + + -- handle errors + if gamedata ~= nil and gamedata.reconnect_requested then + local error_message = core.formspec_escape( + gamedata.errormessage or fgettext("")) + formspec = { + "size[14,8]", + "real_coordinates[true]", + "set_focus[btn_reconnect_yes;true]", + "box[0.5,1.2;13,5;#000]", + ("textarea[0.5,1.2;13,5;;%s;%s]"):format( + fgettext("The server has requested a reconnect:"), error_message), + "button[2,6.6;4,1;btn_reconnect_yes;" .. fgettext("Reconnect") .. "]", + "button[8,6.6;4,1;btn_reconnect_no;" .. fgettext("Main menu") .. "]" + } + ui.overridden = true + elseif gamedata ~= nil and gamedata.errormessage ~= nil then + local error_message = core.formspec_escape(gamedata.errormessage) + + local error_title + if string.find(gamedata.errormessage, "ModError") then + error_title = fgettext("An error occurred in a Lua script:") + else + error_title = fgettext("An error occurred:") + end + formspec = { + "size[14,8]", + "real_coordinates[true]", + "set_focus[btn_error_confirm;true]", + "box[0.5,1.2;13,5;#000]", + ("textarea[0.5,1.2;13,5;;%s;%s]"):format( + error_title, error_message), + "button[5,6.6;4,1;btn_error_confirm;" .. fgettext("OK") .. "]" + } + ui.overridden = true + else + local active_toplevel_ui_elements = 0 + for key,value in pairs(ui.childlist) do + if (value.type == "toplevel") then + local retval = value:get_formspec() + + if retval ~= nil and retval ~= "" then + active_toplevel_ui_elements = active_toplevel_ui_elements + 1 + table.insert(formspec, retval) + end + end + end + + -- no need to show addons if there ain't a toplevel element + if (active_toplevel_ui_elements > 0) then + for key,value in pairs(ui.childlist) do + if (value.type == "addon") then + local retval = value:get_formspec() + + if retval ~= nil and retval ~= "" then + table.insert(formspec, retval) + end + end + end + end + + if (active_toplevel_ui_elements > 1) then + core.log("warning", "more than one active ui ".. + "element, self most likely isn't intended") + end + + if (active_toplevel_ui_elements == 0) then + core.log("warning", "no toplevel ui element ".. + "active; switching to default") + ui.childlist[ui.default]:show() + formspec = {ui.childlist[ui.default]:get_formspec()} + end + end + core.update_formspec(table.concat(formspec)) +end + +-------------------------------------------------------------------------------- +function ui.handle_buttons(fields) + for key,value in pairs(ui.childlist) do + + local retval = value:handle_buttons(fields) + + if retval then + ui.update() + return + end + end +end + + +-------------------------------------------------------------------------------- +function ui.handle_events(event) + + for key,value in pairs(ui.childlist) do + + if value.handle_events ~= nil then + local retval = value:handle_events(event) + + if retval then + return retval + end + end + end +end + +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +-- initialize callbacks +-------------------------------------------------------------------------------- +-------------------------------------------------------------------------------- +core.button_handler = function(fields) + if fields["btn_reconnect_yes"] then + gamedata.reconnect_requested = false + gamedata.errormessage = nil + gamedata.do_reconnect = true + core.start() + return + elseif fields["btn_reconnect_no"] or fields["btn_error_confirm"] then + gamedata.errormessage = nil + gamedata.reconnect_requested = false + ui.update() + return + end + + if ui.handle_buttons(fields) then + ui.update() + end +end + +-------------------------------------------------------------------------------- +core.event_handler = function(event) + -- Handle error messages + if ui.overridden then + if event == "MenuQuit" then + gamedata.errormessage = nil + gamedata.reconnect_requested = false + ui.update() + end + return + end + + if ui.handle_events(event) then + ui.update() + return + end + + if event == "Refresh" then + ui.update() + return + end +end diff --git a/builtin/game/async.lua b/builtin/game/async.lua new file mode 100644 index 0000000..469f179 --- /dev/null +++ b/builtin/game/async.lua @@ -0,0 +1,22 @@ + +core.async_jobs = {} + +function core.async_event_handler(jobid, retval) + local callback = core.async_jobs[jobid] + assert(type(callback) == "function") + callback(unpack(retval, 1, retval.n)) + core.async_jobs[jobid] = nil +end + +function core.handle_async(func, callback, ...) + assert(type(func) == "function" and type(callback) == "function", + "Invalid minetest.handle_async invocation") + local args = {n = select("#", ...), ...} + local mod_origin = core.get_last_run_mod() + + local jobid = core.do_async_callback(func, args, mod_origin) + core.async_jobs[jobid] = callback + + return true +end + diff --git a/builtin/game/auth.lua b/builtin/game/auth.lua new file mode 100644 index 0000000..e7d502b --- /dev/null +++ b/builtin/game/auth.lua @@ -0,0 +1,185 @@ +-- Minetest: builtin/auth.lua + +-- +-- Builtin authentication handler +-- + +-- Make the auth object private, deny access to mods +local core_auth = core.auth +core.auth = nil + +core.builtin_auth_handler = { + get_auth = function(name) + assert(type(name) == "string") + local auth_entry = core_auth.read(name) + -- If no such auth found, return nil + if not auth_entry then + return nil + end + -- Figure out what privileges the player should have. + -- Take a copy of the privilege table + local privileges = {} + for priv, _ in pairs(auth_entry.privileges) do + privileges[priv] = true + end + -- If singleplayer, give all privileges except those marked as give_to_singleplayer = false + if core.is_singleplayer() then + for priv, def in pairs(core.registered_privileges) do + if def.give_to_singleplayer then + privileges[priv] = true + end + end + -- For the admin, give everything + elseif name == core.settings:get("name") then + for priv, def in pairs(core.registered_privileges) do + if def.give_to_admin then + privileges[priv] = true + end + end + end + -- All done + return { + password = auth_entry.password, + privileges = privileges, + last_login = auth_entry.last_login, + } + end, + create_auth = function(name, password) + assert(type(name) == "string") + assert(type(password) == "string") + core.log('info', "Built-in authentication handler adding player '"..name.."'") + return core_auth.create({ + name = name, + password = password, + privileges = core.string_to_privs(core.settings:get("default_privs")), + last_login = -1, -- Defer login time calculation until record_login (called by on_joinplayer) + }) + end, + delete_auth = function(name) + assert(type(name) == "string") + local auth_entry = core_auth.read(name) + if not auth_entry then + return false + end + core.log('info', "Built-in authentication handler deleting player '"..name.."'") + return core_auth.delete(name) + end, + set_password = function(name, password) + assert(type(name) == "string") + assert(type(password) == "string") + local auth_entry = core_auth.read(name) + if not auth_entry then + core.builtin_auth_handler.create_auth(name, password) + else + core.log('info', "Built-in authentication handler setting password of player '"..name.."'") + auth_entry.password = password + core_auth.save(auth_entry) + end + return true + end, + set_privileges = function(name, privileges) + assert(type(name) == "string") + assert(type(privileges) == "table") + local auth_entry = core_auth.read(name) + if not auth_entry then + auth_entry = core.builtin_auth_handler.create_auth(name, + core.get_password_hash(name, + core.settings:get("default_password"))) + end + + auth_entry.privileges = privileges + + core_auth.save(auth_entry) + + -- Run grant callbacks + for priv, _ in pairs(privileges) do + if not auth_entry.privileges[priv] then + core.run_priv_callbacks(name, priv, nil, "grant") + end + end + + -- Run revoke callbacks + for priv, _ in pairs(auth_entry.privileges) do + if not privileges[priv] then + core.run_priv_callbacks(name, priv, nil, "revoke") + end + end + core.notify_authentication_modified(name) + end, + reload = function() + core_auth.reload() + return true + end, + record_login = function(name) + assert(type(name) == "string") + local auth_entry = core_auth.read(name) + assert(auth_entry) + auth_entry.last_login = os.time() + core_auth.save(auth_entry) + end, + iterate = function() + local names = {} + local nameslist = core_auth.list_names() + for k,v in pairs(nameslist) do + names[v] = true + end + return pairs(names) + end, +} + +core.register_on_prejoinplayer(function(name, ip) + if core.registered_auth_handler ~= nil then + return -- Don't do anything if custom auth handler registered + end + local auth_entry = core_auth.read(name) + if auth_entry ~= nil then + return + end + + local name_lower = name:lower() + for k in core.builtin_auth_handler.iterate() do + if k:lower() == name_lower then + return string.format("\nCannot create new player called '%s'. ".. + "Another account called '%s' is already registered. ".. + "Please check the spelling if it's your account ".. + "or use a different nickname.", name, k) + end + end +end) + +-- +-- Authentication API +-- + +function core.register_authentication_handler(handler) + if core.registered_auth_handler then + error("Add-on authentication handler already registered by "..core.registered_auth_handler_modname) + end + core.registered_auth_handler = handler + core.registered_auth_handler_modname = core.get_current_modname() + handler.mod_origin = core.registered_auth_handler_modname +end + +function core.get_auth_handler() + return core.registered_auth_handler or core.builtin_auth_handler +end + +local function auth_pass(name) + return function(...) + local auth_handler = core.get_auth_handler() + if auth_handler[name] then + return auth_handler[name](...) + end + return false + end +end + +core.set_player_password = auth_pass("set_password") +core.set_player_privs = auth_pass("set_privileges") +core.remove_player_auth = auth_pass("delete_auth") +core.auth_reload = auth_pass("reload") + +local record_login = auth_pass("record_login") +core.register_on_joinplayer(function(player) + record_login(player:get_player_name()) +end) diff --git a/builtin/game/chat.lua b/builtin/game/chat.lua new file mode 100644 index 0000000..bbcdcf2 --- /dev/null +++ b/builtin/game/chat.lua @@ -0,0 +1,1359 @@ +-- Minetest: builtin/game/chat.lua + +local S = core.get_translator("__builtin") + +-- Helper function that implements search and replace without pattern matching +-- Returns the string and a boolean indicating whether or not the string was modified +local function safe_gsub(s, replace, with) + local i1, i2 = s:find(replace, 1, true) + if not i1 then + return s, false + end + + return s:sub(1, i1 - 1) .. with .. s:sub(i2 + 1), true +end + +-- +-- Chat message formatter +-- + +-- Implemented in Lua to allow redefinition +function core.format_chat_message(name, message) + local error_str = "Invalid chat message format - missing %s" + local str = core.settings:get("chat_message_format") + local replaced + + -- Name + str, replaced = safe_gsub(str, "@name", name) + if not replaced then + error(error_str:format("@name"), 2) + end + + -- Timestamp + str = safe_gsub(str, "@timestamp", os.date("%H:%M:%S", os.time())) + + -- Insert the message into the string only after finishing all other processing + str, replaced = safe_gsub(str, "@message", message) + if not replaced then + error(error_str:format("@message"), 2) + end + + return str +end + +-- +-- Chat command handler +-- + +core.chatcommands = core.registered_chatcommands -- BACKWARDS COMPATIBILITY + +local msg_time_threshold = + tonumber(core.settings:get("chatcommand_msg_time_threshold")) or 0.1 +core.register_on_chat_message(function(name, message) + if message:sub(1,1) ~= "/" then + return + end + + local cmd, param = string.match(message, "^/([^ ]+) *(.*)") + if not cmd then + core.chat_send_player(name, "-!- "..S("Empty command.")) + return true + end + + param = param or "" + + -- Run core.registered_on_chatcommands callbacks. + if core.run_callbacks(core.registered_on_chatcommands, 5, name, cmd, param) then + return true + end + + local cmd_def = core.registered_chatcommands[cmd] + if not cmd_def then + core.chat_send_player(name, "-!- "..S("Invalid command: @1", cmd)) + return true + end + local has_privs, missing_privs = core.check_player_privs(name, cmd_def.privs) + if has_privs then + core.set_last_run_mod(cmd_def.mod_origin) + local t_before = core.get_us_time() + local success, result = cmd_def.func(name, param) + local delay = (core.get_us_time() - t_before) / 1000000 + if success == false and result == nil then + core.chat_send_player(name, "-!- "..S("Invalid command usage.")) + local help_def = core.registered_chatcommands["help"] + if help_def then + local _, helpmsg = help_def.func(name, cmd) + if helpmsg then + core.chat_send_player(name, helpmsg) + end + end + else + if delay > msg_time_threshold then + -- Show how much time it took to execute the command + if result then + result = result .. core.colorize("#f3d2ff", S(" (@1 s)", + string.format("%.5f", delay))) + else + result = core.colorize("#f3d2ff", S( + "Command execution took @1 s", + string.format("%.5f", delay))) + end + end + if result then + core.chat_send_player(name, result) + end + end + else + core.chat_send_player(name, + S("You don't have permission to run this command " + .. "(missing privileges: @1).", + table.concat(missing_privs, ", "))) + end + return true -- Handled chat message +end) + +if core.settings:get_bool("profiler.load") then + -- Run after register_chatcommand and its register_on_chat_message + -- Before any chatcommands that should be profiled + profiler.init_chatcommand() +end + +-- Parses a "range" string in the format of "here (number)" or +-- "(x1, y1, z1) (x2, y2, z2)", returning two position vectors +local function parse_range_str(player_name, str) + local p1, p2 + local args = str:split(" ") + + if args[1] == "here" then + p1, p2 = core.get_player_radius_area(player_name, tonumber(args[2])) + if p1 == nil then + return false, S("Unable to get position of player @1.", player_name) + end + else + local player = core.get_player_by_name(player_name) + local relpos + if player then + relpos = player:get_pos() + end + p1, p2 = core.string_to_area(str, relpos) + if p1 == nil or p2 == nil then + return false, S("Incorrect area format. " + .. "Expected: (x1,y1,z1) (x2,y2,z2)") + end + end + + return p1, p2 +end + +-- +-- Chat commands +-- +core.register_chatcommand("me", { + params = S(""), + description = S("Show chat action (e.g., '/me orders a pizza' " + .. "displays ' orders a pizza')"), + privs = {shout=true}, + func = function(name, param) + core.chat_send_all("* " .. name .. " " .. param) + return true + end, +}) + +core.register_chatcommand("admin", { + description = S("Show the name of the server owner"), + func = function(name) + local admin = core.settings:get("name") + if admin then + return true, S("The administrator of this server is @1.", admin) + else + return false, S("There's no administrator named " + .. "in the config file.") + end + end, +}) + +local function privileges_of(name, privs) + if not privs then + privs = core.get_player_privs(name) + end + local privstr = core.privs_to_string(privs, ", ") + if privstr == "" then + return S("@1 does not have any privileges.", name) + else + return S("Privileges of @1: @2", name, privstr) + end +end + +core.register_chatcommand("privs", { + params = S("[]"), + description = S("Show privileges of yourself or another player"), + func = function(caller, param) + param = param:trim() + local name = (param ~= "" and param or caller) + if not core.player_exists(name) then + return false, S("Player @1 does not exist.", name) + end + return true, privileges_of(name) + end, +}) + +core.register_chatcommand("haspriv", { + params = S(""), + description = S("Return list of all online players with privilege"), + privs = {basic_privs = true}, + func = function(caller, param) + param = param:trim() + if param == "" then + return false, S("Invalid parameters (see /help haspriv).") + end + if not core.registered_privileges[param] then + return false, S("Unknown privilege!") + end + local privs = core.string_to_privs(param) + local players_with_priv = {} + for _, player in pairs(core.get_connected_players()) do + local player_name = player:get_player_name() + if core.check_player_privs(player_name, privs) then + table.insert(players_with_priv, player_name) + end + end + if #players_with_priv == 0 then + return true, S("No online player has the \"@1\" privilege.", + param) + else + return true, S("Players online with the \"@1\" privilege: @2", + param, + table.concat(players_with_priv, ", ")) + end + end +}) + +local function handle_grant_command(caller, grantname, grantprivstr) + local caller_privs = core.get_player_privs(caller) + if not (caller_privs.privs or caller_privs.basic_privs) then + return false, S("Your privileges are insufficient.") + end + + if not core.get_auth_handler().get_auth(grantname) then + return false, S("Player @1 does not exist.", grantname) + end + local grantprivs = core.string_to_privs(grantprivstr) + if grantprivstr == "all" then + grantprivs = core.registered_privileges + end + local privs = core.get_player_privs(grantname) + local privs_unknown = "" + local basic_privs = + core.string_to_privs(core.settings:get("basic_privs") or "interact,shout") + for priv, _ in pairs(grantprivs) do + if not basic_privs[priv] and not caller_privs.privs then + return false, S("Your privileges are insufficient. ".. + "'@1' only allows you to grant: @2", + "basic_privs", + core.privs_to_string(basic_privs, ', ')) + end + if not core.registered_privileges[priv] then + privs_unknown = privs_unknown .. S("Unknown privilege: @1", priv) .. "\n" + end + privs[priv] = true + end + if privs_unknown ~= "" then + return false, privs_unknown + end + core.set_player_privs(grantname, privs) + for priv, _ in pairs(grantprivs) do + -- call the on_grant callbacks + core.run_priv_callbacks(grantname, priv, caller, "grant") + end + core.log("action", caller..' granted ('..core.privs_to_string(grantprivs, ', ')..') privileges to '..grantname) + if grantname ~= caller then + core.chat_send_player(grantname, + S("@1 granted you privileges: @2", caller, + core.privs_to_string(grantprivs, ', '))) + end + return true, privileges_of(grantname) +end + +core.register_chatcommand("grant", { + params = S(" ( [, [<...>]] | all)"), + description = S("Give privileges to player"), + func = function(name, param) + local grantname, grantprivstr = string.match(param, "([^ ]+) (.+)") + if not grantname or not grantprivstr then + return false, S("Invalid parameters (see /help grant).") + end + return handle_grant_command(name, grantname, grantprivstr) + end, +}) + +core.register_chatcommand("grantme", { + params = S(" [, [<...>]] | all"), + description = S("Grant privileges to yourself"), + func = function(name, param) + if param == "" then + return false, S("Invalid parameters (see /help grantme).") + end + return handle_grant_command(name, name, param) + end, +}) + +local function handle_revoke_command(caller, revokename, revokeprivstr) + local caller_privs = core.get_player_privs(caller) + if not (caller_privs.privs or caller_privs.basic_privs) then + return false, S("Your privileges are insufficient.") + end + + if not core.get_auth_handler().get_auth(revokename) then + return false, S("Player @1 does not exist.", revokename) + end + + local privs = core.get_player_privs(revokename) + + local revokeprivs = core.string_to_privs(revokeprivstr) + local is_singleplayer = core.is_singleplayer() + local is_admin = not is_singleplayer + and revokename == core.settings:get("name") + and revokename ~= "" + if revokeprivstr == "all" then + revokeprivs = table.copy(privs) + end + + local privs_unknown = "" + local basic_privs = + core.string_to_privs(core.settings:get("basic_privs") or "interact,shout") + local irrevokable = {} + local has_irrevokable_priv = false + for priv, _ in pairs(revokeprivs) do + if not basic_privs[priv] and not caller_privs.privs then + return false, S("Your privileges are insufficient. ".. + "'@1' only allows you to revoke: @2", + "basic_privs", + core.privs_to_string(basic_privs, ', ')) + end + local def = core.registered_privileges[priv] + if not def then + -- Old/removed privileges might still be granted to certain players + if not privs[priv] then + privs_unknown = privs_unknown .. S("Unknown privilege: @1", priv) .. "\n" + end + elseif is_singleplayer and def.give_to_singleplayer then + irrevokable[priv] = true + elseif is_admin and def.give_to_admin then + irrevokable[priv] = true + end + end + for priv, _ in pairs(irrevokable) do + revokeprivs[priv] = nil + has_irrevokable_priv = true + end + if privs_unknown ~= "" then + return false, privs_unknown + end + if has_irrevokable_priv then + if is_singleplayer then + core.chat_send_player(caller, + S("Note: Cannot revoke in singleplayer: @1", + core.privs_to_string(irrevokable, ', '))) + elseif is_admin then + core.chat_send_player(caller, + S("Note: Cannot revoke from admin: @1", + core.privs_to_string(irrevokable, ', '))) + end + end + + local revokecount = 0 + for priv, _ in pairs(revokeprivs) do + privs[priv] = nil + revokecount = revokecount + 1 + end + + if revokecount == 0 then + return false, S("No privileges were revoked.") + end + + core.set_player_privs(revokename, privs) + for priv, _ in pairs(revokeprivs) do + -- call the on_revoke callbacks + core.run_priv_callbacks(revokename, priv, caller, "revoke") + end + local new_privs = core.get_player_privs(revokename) + + core.log("action", caller..' revoked (' + ..core.privs_to_string(revokeprivs, ', ') + ..') privileges from '..revokename) + if revokename ~= caller then + core.chat_send_player(revokename, + S("@1 revoked privileges from you: @2", caller, + core.privs_to_string(revokeprivs, ', '))) + end + return true, privileges_of(revokename, new_privs) +end + +core.register_chatcommand("revoke", { + params = S(" ( [, [<...>]] | all)"), + description = S("Remove privileges from player"), + privs = {}, + func = function(name, param) + local revokename, revokeprivstr = string.match(param, "([^ ]+) (.+)") + if not revokename or not revokeprivstr then + return false, S("Invalid parameters (see /help revoke).") + end + return handle_revoke_command(name, revokename, revokeprivstr) + end, +}) + +core.register_chatcommand("revokeme", { + params = S(" [, [<...>]] | all"), + description = S("Revoke privileges from yourself"), + privs = {}, + func = function(name, param) + if param == "" then + return false, S("Invalid parameters (see /help revokeme).") + end + return handle_revoke_command(name, name, param) + end, +}) + +core.register_chatcommand("setpassword", { + params = S(" "), + description = S("Set player's password"), + privs = {password=true}, + func = function(name, param) + local toname, raw_password = string.match(param, "^([^ ]+) +(.+)$") + if not toname then + toname = param:match("^([^ ]+) *$") + raw_password = nil + end + + if not toname then + return false, S("Name field required.") + end + + local msg_chat, msg_log, msg_ret + if not raw_password then + core.set_player_password(toname, "") + msg_chat = S("Your password was cleared by @1.", name) + msg_log = name .. " clears password of " .. toname .. "." + msg_ret = S("Password of player \"@1\" cleared.", toname) + else + core.set_player_password(toname, + core.get_password_hash(toname, + raw_password)) + msg_chat = S("Your password was set by @1.", name) + msg_log = name .. " sets password of " .. toname .. "." + msg_ret = S("Password of player \"@1\" set.", toname) + end + + if toname ~= name then + core.chat_send_player(toname, msg_chat) + end + + core.log("action", msg_log) + + return true, msg_ret + end, +}) + +core.register_chatcommand("clearpassword", { + params = S(""), + description = S("Set empty password for a player"), + privs = {password=true}, + func = function(name, param) + local toname = param + if toname == "" then + return false, S("Name field required.") + end + core.set_player_password(toname, '') + + core.log("action", name .. " clears password of " .. toname .. ".") + + return true, S("Password of player \"@1\" cleared.", toname) + end, +}) + +core.register_chatcommand("auth_reload", { + params = "", + description = S("Reload authentication data"), + privs = {server=true}, + func = function(name, param) + local done = core.auth_reload() + return done, (done and S("Done.") or S("Failed.")) + end, +}) + +core.register_chatcommand("remove_player", { + params = S(""), + description = S("Remove a player's data"), + privs = {server=true}, + func = function(name, param) + local toname = param + if toname == "" then + return false, S("Name field required.") + end + + local rc = core.remove_player(toname) + + if rc == 0 then + core.log("action", name .. " removed player data of " .. toname .. ".") + return true, S("Player \"@1\" removed.", toname) + elseif rc == 1 then + return true, S("No such player \"@1\" to remove.", toname) + elseif rc == 2 then + return true, S("Player \"@1\" is connected, cannot remove.", toname) + end + + return false, S("Unhandled remove_player return code @1.", tostring(rc)) + end, +}) + + +-- pos may be a non-integer position +local function find_free_position_near(pos) + local tries = { + vector.new( 1, 0, 0), + vector.new(-1, 0, 0), + vector.new( 0, 0, 1), + vector.new( 0, 0, -1), + } + for _, d in ipairs(tries) do + local p = vector.add(pos, d) + local n = core.get_node_or_nil(p) + if n then + local def = core.registered_nodes[n.name] + if def and not def.walkable then + return p + end + end + end + return pos +end + +-- Teleports player to

if possible +local function teleport_to_pos(name, p) + local lm = 31007 -- equals MAX_MAP_GENERATION_LIMIT in C++ + if p.x < -lm or p.x > lm or p.y < -lm or p.y > lm + or p.z < -lm or p.z > lm then + return false, S("Cannot teleport out of map bounds!") + end + local teleportee = core.get_player_by_name(name) + if not teleportee then + return false, S("Cannot get player with name @1.", name) + end + if teleportee:get_attach() then + return false, S("Cannot teleport, @1 " .. + "is attached to an object!", name) + end + teleportee:set_pos(p) + return true, S("Teleporting @1 to @2.", name, core.pos_to_string(p, 1)) +end + +-- Teleports player next to player if possible +local function teleport_to_player(name, target_name) + if name == target_name then + return false, S("One does not teleport to oneself.") + end + local teleportee = core.get_player_by_name(name) + if not teleportee then + return false, S("Cannot get teleportee with name @1.", name) + end + if teleportee:get_attach() then + return false, S("Cannot teleport, @1 " .. + "is attached to an object!", name) + end + local target = core.get_player_by_name(target_name) + if not target then + return false, S("Cannot get target player with name @1.", target_name) + end + local p = find_free_position_near(target:get_pos()) + teleportee:set_pos(p) + return true, S("Teleporting @1 to @2 at @3.", name, target_name, + core.pos_to_string(p, 1)) +end + +core.register_chatcommand("teleport", { + params = S(",, | | ,, | "), + description = S("Teleport to position or player"), + privs = {teleport=true}, + func = function(name, param) + local player = core.get_player_by_name(name) + local relpos + if player then + relpos = player:get_pos() + end + local p = {} + p.x, p.y, p.z = string.match(param, "^([%d.~-]+)[, ] *([%d.~-]+)[, ] *([%d.~-]+)$") + p = core.parse_coordinates(p.x, p.y, p.z, relpos) + if p and p.x and p.y and p.z then + return teleport_to_pos(name, p) + end + + local target_name = param:match("^([^ ]+)$") + if target_name then + return teleport_to_player(name, target_name) + end + + local has_bring_priv = core.check_player_privs(name, {bring=true}) + local missing_bring_msg = S("You don't have permission to teleport " .. + "other players (missing privilege: @1).", "bring") + + local teleportee_name + p = {} + teleportee_name, p.x, p.y, p.z = param:match( + "^([^ ]+) +([%d.~-]+)[, ] *([%d.~-]+)[, ] *([%d.~-]+)$") + if teleportee_name then + local teleportee = core.get_player_by_name(teleportee_name) + if not teleportee then + return + end + relpos = teleportee:get_pos() + p = core.parse_coordinates(p.x, p.y, p.z, relpos) + end + p = vector.apply(p, tonumber) + + if teleportee_name and p.x and p.y and p.z then + if not has_bring_priv then + return false, missing_bring_msg + end + return teleport_to_pos(teleportee_name, p) + end + + teleportee_name, target_name = string.match(param, "^([^ ]+) +([^ ]+)$") + if teleportee_name and target_name then + if not has_bring_priv then + return false, missing_bring_msg + end + return teleport_to_player(teleportee_name, target_name) + end + + return false + end, +}) + +core.register_chatcommand("set", { + params = S("([-n] ) | "), + description = S("Set or read server configuration setting"), + privs = {server=true}, + func = function(name, param) + local arg, setname, setvalue = string.match(param, "(-[n]) ([^ ]+) (.+)") + if arg and arg == "-n" and setname and setvalue then + core.settings:set(setname, setvalue) + return true, setname .. " = " .. setvalue + end + + setname, setvalue = string.match(param, "([^ ]+) (.+)") + if setname and setvalue then + if setname:sub(1, 7) == "secure." then + return false, S("Failed. Cannot modify secure settings. " + .. "Edit the settings file manually.") + end + if not core.settings:get(setname) then + return false, S("Failed. Use '/set -n ' " + .. "to create a new setting.") + end + core.settings:set(setname, setvalue) + return true, S("@1 = @2", setname, setvalue) + end + + setname = string.match(param, "([^ ]+)") + if setname then + setvalue = core.settings:get(setname) + if not setvalue then + setvalue = S("") + end + return true, S("@1 = @2", setname, setvalue) + end + + return false, S("Invalid parameters (see /help set).") + end, +}) + +local function emergeblocks_callback(pos, action, num_calls_remaining, ctx) + if ctx.total_blocks == 0 then + ctx.total_blocks = num_calls_remaining + 1 + ctx.current_blocks = 0 + end + ctx.current_blocks = ctx.current_blocks + 1 + + if ctx.current_blocks == ctx.total_blocks then + core.chat_send_player(ctx.requestor_name, + S("Finished emerging @1 blocks in @2ms.", + ctx.total_blocks, + string.format("%.2f", (os.clock() - ctx.start_time) * 1000))) + end +end + +local function emergeblocks_progress_update(ctx) + if ctx.current_blocks ~= ctx.total_blocks then + core.chat_send_player(ctx.requestor_name, + S("emergeblocks update: @1/@2 blocks emerged (@3%)", + ctx.current_blocks, ctx.total_blocks, + string.format("%.1f", (ctx.current_blocks / ctx.total_blocks) * 100))) + + core.after(2, emergeblocks_progress_update, ctx) + end +end + +core.register_chatcommand("emergeblocks", { + params = S("(here []) | ( )"), + description = S("Load (or, if nonexistent, generate) map blocks contained in " + .. "area pos1 to pos2 ( and must be in parentheses)"), + privs = {server=true}, + func = function(name, param) + local p1, p2 = parse_range_str(name, param) + if p1 == false then + return false, p2 + end + + local context = { + current_blocks = 0, + total_blocks = 0, + start_time = os.clock(), + requestor_name = name + } + + core.emerge_area(p1, p2, emergeblocks_callback, context) + core.after(2, emergeblocks_progress_update, context) + + return true, S("Started emerge of area ranging from @1 to @2.", + core.pos_to_string(p1, 1), core.pos_to_string(p2, 1)) + end, +}) + +core.register_chatcommand("deleteblocks", { + params = S("(here []) | ( )"), + description = S("Delete map blocks contained in area pos1 to pos2 " + .. "( and must be in parentheses)"), + privs = {server=true}, + func = function(name, param) + local p1, p2 = parse_range_str(name, param) + if p1 == false then + return false, p2 + end + + if core.delete_area(p1, p2) then + return true, S("Successfully cleared area " + .. "ranging from @1 to @2.", + core.pos_to_string(p1, 1), core.pos_to_string(p2, 1)) + else + return false, S("Failed to clear one or more " + .. "blocks in area.") + end + end, +}) + +core.register_chatcommand("fixlight", { + params = S("(here []) | ( )"), + description = S("Resets lighting in the area between pos1 and pos2 " + .. "( and must be in parentheses)"), + privs = {server = true}, + func = function(name, param) + local p1, p2 = parse_range_str(name, param) + if p1 == false then + return false, p2 + end + + if core.fix_light(p1, p2) then + return true, S("Successfully reset light in the area " + .. "ranging from @1 to @2.", + core.pos_to_string(p1, 1), core.pos_to_string(p2, 1)) + else + return false, S("Failed to load one or more blocks in area.") + end + end, +}) + +core.register_chatcommand("mods", { + params = "", + description = S("List mods installed on the server"), + privs = {}, + func = function(name, param) + local mods = core.get_modnames() + if #mods == 0 then + return true, S("No mods installed.") + else + return true, table.concat(core.get_modnames(), ", ") + end + end, +}) + +local function handle_give_command(cmd, giver, receiver, stackstring) + core.log("action", giver .. " invoked " .. cmd + .. ', stackstring="' .. stackstring .. '"') + local itemstack = ItemStack(stackstring) + if itemstack:is_empty() then + return false, S("Cannot give an empty item.") + elseif (not itemstack:is_known()) or (itemstack:get_name() == "unknown") then + return false, S("Cannot give an unknown item.") + -- Forbid giving 'ignore' due to unwanted side effects + elseif itemstack:get_name() == "ignore" then + return false, S("Giving 'ignore' is not allowed.") + end + local receiverref = core.get_player_by_name(receiver) + if receiverref == nil then + return false, S("@1 is not a known player.", receiver) + end + local leftover = receiverref:get_inventory():add_item("main", itemstack) + local partiality + if leftover:is_empty() then + partiality = nil + elseif leftover:get_count() == itemstack:get_count() then + partiality = false + else + partiality = true + end + -- The actual item stack string may be different from what the "giver" + -- entered (e.g. big numbers are always interpreted as 2^16-1). + stackstring = itemstack:to_string() + local msg + if partiality == true then + msg = S("@1 partially added to inventory.", stackstring) + elseif partiality == false then + msg = S("@1 could not be added to inventory.", stackstring) + else + msg = S("@1 added to inventory.", stackstring) + end + if giver == receiver then + return true, msg + else + core.chat_send_player(receiver, msg) + local msg_other + if partiality == true then + msg_other = S("@1 partially added to inventory of @2.", + stackstring, receiver) + elseif partiality == false then + msg_other = S("@1 could not be added to inventory of @2.", + stackstring, receiver) + else + msg_other = S("@1 added to inventory of @2.", + stackstring, receiver) + end + return true, msg_other + end +end + +core.register_chatcommand("give", { + params = S(" [ []]"), + description = S("Give item to player"), + privs = {give=true}, + func = function(name, param) + local toname, itemstring = string.match(param, "^([^ ]+) +(.+)$") + if not toname or not itemstring then + return false, S("Name and ItemString required.") + end + return handle_give_command("/give", name, toname, itemstring) + end, +}) + +core.register_chatcommand("giveme", { + params = S(" [ []]"), + description = S("Give item to yourself"), + privs = {give=true}, + func = function(name, param) + local itemstring = string.match(param, "(.+)$") + if not itemstring then + return false, S("ItemString required.") + end + return handle_give_command("/giveme", name, name, itemstring) + end, +}) + +core.register_chatcommand("spawnentity", { + params = S(" [,,]"), + description = S("Spawn entity at given (or your) position"), + privs = {give=true, interact=true}, + func = function(name, param) + local entityname, pstr = string.match(param, "^([^ ]+) *(.*)$") + if not entityname then + return false, S("EntityName required.") + end + core.log("action", ("%s invokes /spawnentity, entityname=%q") + :format(name, entityname)) + local player = core.get_player_by_name(name) + if player == nil then + core.log("error", "Unable to spawn entity, player is nil") + return false, S("Unable to spawn entity, player is nil.") + end + if not core.registered_entities[entityname] then + return false, S("Cannot spawn an unknown entity.") + end + local p + if pstr == "" then + p = player:get_pos() + else + p = {} + p.x, p.y, p.z = string.match(pstr, "^([%d.~-]+)[, ] *([%d.~-]+)[, ] *([%d.~-]+)$") + local relpos = player:get_pos() + p = core.parse_coordinates(p.x, p.y, p.z, relpos) + if not (p and p.x and p.y and p.z) then + return false, S("Invalid parameters (@1).", param) + end + end + p.y = p.y + 1 + local obj = core.add_entity(p, entityname) + if obj then + return true, S("@1 spawned.", entityname) + else + return true, S("@1 failed to spawn.", entityname) + end + end, +}) + +core.register_chatcommand("pulverize", { + params = "", + description = S("Destroy item in hand"), + func = function(name, param) + local player = core.get_player_by_name(name) + if not player then + core.log("error", "Unable to pulverize, no player.") + return false, S("Unable to pulverize, no player.") + end + local wielded_item = player:get_wielded_item() + if wielded_item:is_empty() then + return false, S("Unable to pulverize, no item in hand.") + end + core.log("action", name .. " pulverized \"" .. + wielded_item:get_name() .. " " .. wielded_item:get_count() .. "\"") + player:set_wielded_item(nil) + return true, S("An item was pulverized.") + end, +}) + +-- Key = player name +core.rollback_punch_callbacks = {} + +core.register_on_punchnode(function(pos, node, puncher) + local name = puncher and puncher:get_player_name() + if name and core.rollback_punch_callbacks[name] then + core.rollback_punch_callbacks[name](pos, node, puncher) + core.rollback_punch_callbacks[name] = nil + end +end) + +core.register_chatcommand("rollback_check", { + params = S("[] [] []"), + description = S("Check who last touched a node or a node near it " + .. "within the time specified by . " + .. "Default: range = 0, seconds = 86400 = 24h, limit = 5. " + .. "Set to inf for no time limit"), + privs = {rollback=true}, + func = function(name, param) + if not core.settings:get_bool("enable_rollback_recording") then + return false, S("Rollback functions are disabled.") + end + local range, seconds, limit = + param:match("(%d+) *(%d*) *(%d*)") + range = tonumber(range) or 0 + seconds = tonumber(seconds) or 86400 + limit = tonumber(limit) or 5 + if limit > 100 then + return false, S("That limit is too high!") + end + + core.rollback_punch_callbacks[name] = function(pos, node, puncher) + local name = puncher:get_player_name() + core.chat_send_player(name, S("Checking @1 ...", core.pos_to_string(pos))) + local actions = core.rollback_get_node_actions(pos, range, seconds, limit) + if not actions then + core.chat_send_player(name, S("Rollback functions are disabled.")) + return + end + local num_actions = #actions + if num_actions == 0 then + core.chat_send_player(name, + S("Nobody has touched the specified " + .. "location in @1 seconds.", + seconds)) + return + end + local time = os.time() + for i = num_actions, 1, -1 do + local action = actions[i] + core.chat_send_player(name, + S("@1 @2 @3 -> @4 @5 seconds ago.", + core.pos_to_string(action.pos), + action.actor, + action.oldnode.name, + action.newnode.name, + time - action.time)) + end + end + + return true, S("Punch a node (range=@1, seconds=@2, limit=@3).", + range, seconds, limit) + end, +}) + +core.register_chatcommand("rollback", { + params = S("( []) | (: [])"), + description = S("Revert actions of a player. " + .. "Default for is 60. " + .. "Set to inf for no time limit"), + privs = {rollback=true}, + func = function(name, param) + if not core.settings:get_bool("enable_rollback_recording") then + return false, S("Rollback functions are disabled.") + end + local target_name, seconds = string.match(param, ":([^ ]+) *(%d*)") + local rev_msg + if not target_name then + local player_name + player_name, seconds = string.match(param, "([^ ]+) *(%d*)") + if not player_name then + return false, S("Invalid parameters. " + .. "See /help rollback and " + .. "/help rollback_check.") + end + seconds = tonumber(seconds) or 60 + target_name = "player:"..player_name + rev_msg = S("Reverting actions of player '@1' since @2 seconds.", + player_name, seconds) + else + seconds = tonumber(seconds) or 60 + rev_msg = S("Reverting actions of @1 since @2 seconds.", + target_name, seconds) + end + core.chat_send_player(name, rev_msg) + local success, log = core.rollback_revert_actions_by( + target_name, seconds) + local response = "" + if #log > 100 then + response = S("(log is too long to show)").."\n" + else + for _, line in pairs(log) do + response = response .. line .. "\n" + end + end + if success then + response = response .. S("Reverting actions succeeded.") + else + response = response .. S("Reverting actions FAILED.") + end + return success, response + end, +}) + +core.register_chatcommand("status", { + description = S("Show server status"), + func = function(name, param) + local status = core.get_server_status(name, false) + if status and status ~= "" then + return true, status + end + return false, S("This command was disabled by a mod or game.") + end, +}) + +local function get_time(timeofday) + local time = math.floor(timeofday * 1440) + local minute = time % 60 + local hour = (time - minute) / 60 + return time, hour, minute +end + +core.register_chatcommand("time", { + params = S("[<0..23>:<0..59> | <0..24000>]"), + description = S("Show or set time of day"), + privs = {}, + func = function(name, param) + if param == "" then + local current_time = math.floor(core.get_timeofday() * 1440) + local minutes = current_time % 60 + local hour = (current_time - minutes) / 60 + return true, S("Current time is @1:@2.", + string.format("%d", hour), + string.format("%02d", minutes)) + end + local player_privs = core.get_player_privs(name) + if not player_privs.settime then + return false, S("You don't have permission to run " + .. "this command (missing privilege: @1).", "settime") + end + local relative, negative, hour, minute = param:match("^(~?)(%-?)(%d+):(%d+)$") + if not relative then -- checking the first capture against nil suffices + local new_time = core.parse_relative_number(param, core.get_timeofday() * 24000) + if not new_time then + new_time = tonumber(param) or -1 + else + new_time = new_time % 24000 + end + if new_time ~= new_time or new_time < 0 or new_time > 24000 then + return false, S("Invalid time (must be between 0 and 24000).") + end + core.set_timeofday(new_time / 24000) + core.log("action", name .. " sets time to " .. new_time) + return true, S("Time of day changed.") + end + local new_time + hour = tonumber(hour) + minute = tonumber(minute) + if relative == "" then + if hour < 0 or hour > 23 then + return false, S("Invalid hour (must be between 0 and 23 inclusive).") + elseif minute < 0 or minute > 59 then + return false, S("Invalid minute (must be between 0 and 59 inclusive).") + end + new_time = (hour * 60 + minute) / 1440 + else + if minute < 0 or minute > 59 then + return false, S("Invalid minute (must be between 0 and 59 inclusive).") + end + local current_time = core.get_timeofday() + if negative == "-" then -- negative time + hour, minute = -hour, -minute + end + new_time = (current_time + (hour * 60 + minute) / 1440) % 1 + local _ + _, hour, minute = get_time(new_time) + end + core.set_timeofday(new_time) + core.log("action", ("%s sets time to %d:%02d"):format(name, hour, minute)) + return true, S("Time of day changed.") + end, +}) + +core.register_chatcommand("days", { + description = S("Show day count since world creation"), + func = function(name, param) + return true, S("Current day is @1.", core.get_day_count()) + end +}) + +local function parse_shutdown_param(param) + local delay, reconnect, message + local one, two, three + one, two, three = param:match("^(%S+) +(%-r) +(.*)") + if one and two and three then + -- 3 arguments: delay, reconnect and message + return one, two, three + end + -- 2 arguments + one, two = param:match("^(%S+) +(.*)") + if one and two then + if tonumber(one) then + delay = one + if two == "-r" then + reconnect = two + else + message = two + end + elseif one == "-r" then + reconnect, message = one, two + end + return delay, reconnect, message + end + -- 1 argument + one = param:match("(.*)") + if tonumber(one) then + delay = one + elseif one == "-r" then + reconnect = one + else + message = one + end + return delay, reconnect, message +end + +core.register_chatcommand("shutdown", { + params = S("[ | -1] [-r] []"), + description = S("Shutdown server (-1 cancels a delayed shutdown, -r allows players to reconnect)"), + privs = {server=true}, + func = function(name, param) + local delay, reconnect, message = parse_shutdown_param(param) + local bool_reconnect = reconnect == "-r" + if not message then + message = "" + end + delay = tonumber(delay) or 0 + + if delay == 0 then + core.log("action", name .. " shuts down server") + core.chat_send_all("*** "..S("Server shutting down (operator request).")) + end + core.request_shutdown(message:trim(), bool_reconnect, delay) + return true + end, +}) + +core.register_chatcommand("ban", { + params = S("[]"), + description = S("Ban the IP of a player or show the ban list"), + privs = {ban=true}, + func = function(name, param) + if param == "" then + local ban_list = core.get_ban_list() + if ban_list == "" then + return true, S("The ban list is empty.") + else + return true, S("Ban list: @1", ban_list) + end + end + if core.is_singleplayer() then + return false, S("You cannot ban players in singleplayer!") + end + if not core.get_player_by_name(param) then + return false, S("Player is not online.") + end + if not core.ban_player(param) then + return false, S("Failed to ban player.") + end + local desc = core.get_ban_description(param) + core.log("action", name .. " bans " .. desc .. ".") + return true, S("Banned @1.", desc) + end, +}) + +core.register_chatcommand("unban", { + params = S(" | "), + description = S("Remove IP ban belonging to a player/IP"), + privs = {ban=true}, + func = function(name, param) + if not core.unban_player_or_ip(param) then + return false, S("Failed to unban player/IP.") + end + core.log("action", name .. " unbans " .. param) + return true, S("Unbanned @1.", param) + end, +}) + +core.register_chatcommand("kick", { + params = S(" []"), + description = S("Kick a player"), + privs = {kick=true}, + func = function(name, param) + local tokick, reason = param:match("([^ ]+) (.+)") + tokick = tokick or param + if not core.kick_player(tokick, reason) then + return false, S("Failed to kick player @1.", tokick) + end + local log_reason = "" + if reason then + log_reason = " with reason \"" .. reason .. "\"" + end + core.log("action", name .. " kicks " .. tokick .. log_reason) + return true, S("Kicked @1.", tokick) + end, +}) + +core.register_chatcommand("clearobjects", { + params = S("[full | quick]"), + description = S("Clear all objects in world"), + privs = {server=true}, + func = function(name, param) + local options = {} + if param == "" or param == "quick" then + options.mode = "quick" + elseif param == "full" then + options.mode = "full" + else + return false, S("Invalid usage, see /help clearobjects.") + end + + core.log("action", name .. " clears objects (" + .. options.mode .. " mode).") + if options.mode == "full" then + core.chat_send_all(S("Clearing all objects. This may take a long time. " + .. "You may experience a timeout. (by @1)", name)) + end + core.clear_objects(options) + core.log("action", "Object clearing done.") + core.chat_send_all("*** "..S("Cleared all objects.")) + return true + end, +}) + +core.register_chatcommand("msg", { + params = S(" "), + description = S("Send a direct message to a player"), + privs = {shout=true}, + func = function(name, param) + local sendto, message = param:match("^(%S+)%s(.+)$") + if not sendto then + return false, S("Invalid usage, see /help msg.") + end + if not core.get_player_by_name(sendto) then + return false, S("The player @1 is not online.", sendto) + end + core.log("action", "DM from " .. name .. " to " .. sendto + .. ": " .. message) + core.chat_send_player(sendto, S("DM from @1: @2", name, message)) + return true, S("Message sent.") + end, +}) + +core.register_chatcommand("last-login", { + params = S("[]"), + description = S("Get the last login time of a player or yourself"), + func = function(name, param) + if param == "" then + param = name + end + local pauth = core.get_auth_handler().get_auth(param) + if pauth and pauth.last_login and pauth.last_login ~= -1 then + -- Time in UTC, ISO 8601 format + return true, S("@1's last login time was @2.", + param, + os.date("!%Y-%m-%dT%H:%M:%SZ", pauth.last_login)) + end + return false, S("@1's last login time is unknown.", param) + end, +}) + +core.register_chatcommand("clearinv", { + params = S("[]"), + description = S("Clear the inventory of yourself or another player"), + func = function(name, param) + local player + if param and param ~= "" and param ~= name then + if not core.check_player_privs(name, {server=true}) then + return false, S("You don't have permission to " + .. "clear another player's inventory " + .. "(missing privilege: @1).", "server") + end + player = core.get_player_by_name(param) + core.chat_send_player(param, S("@1 cleared your inventory.", name)) + else + player = core.get_player_by_name(name) + end + + if player then + player:get_inventory():set_list("main", {}) + player:get_inventory():set_list("craft", {}) + player:get_inventory():set_list("craftpreview", {}) + core.log("action", name.." clears "..player:get_player_name().."'s inventory") + return true, S("Cleared @1's inventory.", player:get_player_name()) + else + return false, S("Player must be online to clear inventory!") + end + end, +}) + +local function handle_kill_command(killer, victim) + if core.settings:get_bool("enable_damage") == false then + return false, S("Players can't be killed, damage has been disabled.") + end + local victimref = core.get_player_by_name(victim) + if victimref == nil then + return false, S("Player @1 is not online.", victim) + elseif victimref:get_hp() <= 0 then + if killer == victim then + return false, S("You are already dead.") + else + return false, S("@1 is already dead.", victim) + end + end + if killer ~= victim then + core.log("action", string.format("%s killed %s", killer, victim)) + end + -- Kill victim + victimref:set_hp(0) + return true, S("@1 has been killed.", victim) +end + +core.register_chatcommand("kill", { + params = S("[]"), + description = S("Kill player or yourself"), + privs = {server=true}, + func = function(name, param) + return handle_kill_command(name, param == "" and name or param) + end, +}) diff --git a/builtin/game/constants.lua b/builtin/game/constants.lua new file mode 100644 index 0000000..54eeea5 --- /dev/null +++ b/builtin/game/constants.lua @@ -0,0 +1,31 @@ +-- Minetest: builtin/constants.lua + +-- +-- Constants values for use with the Lua API +-- + +-- mapnode.h +-- Built-in Content IDs (for use with VoxelManip API) +core.CONTENT_UNKNOWN = 125 +core.CONTENT_AIR = 126 +core.CONTENT_IGNORE = 127 + +-- emerge.h +-- Block emerge status constants (for use with core.emerge_area) +core.EMERGE_CANCELLED = 0 +core.EMERGE_ERRORED = 1 +core.EMERGE_FROM_MEMORY = 2 +core.EMERGE_FROM_DISK = 3 +core.EMERGE_GENERATED = 4 + +-- constants.h +-- Size of mapblocks in nodes +core.MAP_BLOCKSIZE = 16 +-- Default maximal HP of a player +core.PLAYER_MAX_HP_DEFAULT = 20 +-- Default maximal breath of a player +core.PLAYER_MAX_BREATH_DEFAULT = 10 + +-- light.h +-- Maximum value for node 'light_source' parameter +core.LIGHT_MAX = 14 diff --git a/builtin/game/deprecated.lua b/builtin/game/deprecated.lua new file mode 100644 index 0000000..c5c7848 --- /dev/null +++ b/builtin/game/deprecated.lua @@ -0,0 +1,65 @@ +-- Minetest: builtin/deprecated.lua + +-- +-- EnvRef +-- +core.env = {} +local envref_deprecation_message_printed = false +setmetatable(core.env, { + __index = function(table, key) + if not envref_deprecation_message_printed then + core.log("deprecated", "core.env:[...] is deprecated and should be replaced with core.[...]") + envref_deprecation_message_printed = true + end + local func = core[key] + if type(func) == "function" then + rawset(table, key, function(self, ...) + return func(...) + end) + else + rawset(table, key, nil) + end + return rawget(table, key) + end +}) + +function core.rollback_get_last_node_actor(pos, range, seconds) + return core.rollback_get_node_actions(pos, range, seconds, 1)[1] +end + +-- +-- core.setting_* +-- + +local settings = core.settings + +local function setting_proxy(name) + return function(...) + core.log("deprecated", "WARNING: minetest.setting_* ".. + "functions are deprecated. ".. + "Use methods on the minetest.settings object.") + return settings[name](settings, ...) + end +end + +core.setting_set = setting_proxy("set") +core.setting_get = setting_proxy("get") +core.setting_setbool = setting_proxy("set_bool") +core.setting_getbool = setting_proxy("get_bool") +core.setting_save = setting_proxy("write") + +-- +-- core.register_on_auth_fail +-- + +function core.register_on_auth_fail(func) + core.log("deprecated", "core.register_on_auth_fail " .. + "is deprecated and should be replaced by " .. + "core.register_on_authplayer instead.") + + core.register_on_authplayer(function (player_name, ip, is_success) + if not is_success then + func(player_name, ip) + end + end) +end diff --git a/builtin/game/detached_inventory.lua b/builtin/game/detached_inventory.lua new file mode 100644 index 0000000..2e27168 --- /dev/null +++ b/builtin/game/detached_inventory.lua @@ -0,0 +1,24 @@ +-- Minetest: builtin/detached_inventory.lua + +core.detached_inventories = {} + +function core.create_detached_inventory(name, callbacks, player_name) + local stuff = {} + stuff.name = name + if callbacks then + stuff.allow_move = callbacks.allow_move + stuff.allow_put = callbacks.allow_put + stuff.allow_take = callbacks.allow_take + stuff.on_move = callbacks.on_move + stuff.on_put = callbacks.on_put + stuff.on_take = callbacks.on_take + end + stuff.mod_origin = core.get_current_modname() or "??" + core.detached_inventories[name] = stuff + return core.create_detached_inventory_raw(name, player_name) +end + +function core.remove_detached_inventory(name) + core.detached_inventories[name] = nil + return core.remove_detached_inventory_raw(name) +end diff --git a/builtin/game/falling.lua b/builtin/game/falling.lua new file mode 100644 index 0000000..d5727f2 --- /dev/null +++ b/builtin/game/falling.lua @@ -0,0 +1,608 @@ +-- Minetest: builtin/item.lua + +local builtin_shared = ... +local SCALE = 0.667 + +local facedir_to_euler = { + {y = 0, x = 0, z = 0}, + {y = -math.pi/2, x = 0, z = 0}, + {y = math.pi, x = 0, z = 0}, + {y = math.pi/2, x = 0, z = 0}, + {y = math.pi/2, x = -math.pi/2, z = math.pi/2}, + {y = math.pi/2, x = math.pi, z = math.pi/2}, + {y = math.pi/2, x = math.pi/2, z = math.pi/2}, + {y = math.pi/2, x = 0, z = math.pi/2}, + {y = -math.pi/2, x = math.pi/2, z = math.pi/2}, + {y = -math.pi/2, x = 0, z = math.pi/2}, + {y = -math.pi/2, x = -math.pi/2, z = math.pi/2}, + {y = -math.pi/2, x = math.pi, z = math.pi/2}, + {y = 0, x = 0, z = math.pi/2}, + {y = 0, x = -math.pi/2, z = math.pi/2}, + {y = 0, x = math.pi, z = math.pi/2}, + {y = 0, x = math.pi/2, z = math.pi/2}, + {y = math.pi, x = math.pi, z = math.pi/2}, + {y = math.pi, x = math.pi/2, z = math.pi/2}, + {y = math.pi, x = 0, z = math.pi/2}, + {y = math.pi, x = -math.pi/2, z = math.pi/2}, + {y = math.pi, x = math.pi, z = 0}, + {y = -math.pi/2, x = math.pi, z = 0}, + {y = 0, x = math.pi, z = 0}, + {y = math.pi/2, x = math.pi, z = 0} +} + +local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81 + +-- +-- Falling stuff +-- + +core.register_entity(":__builtin:falling_node", { + initial_properties = { + visual = "item", + visual_size = vector.new(SCALE, SCALE, SCALE), + textures = {}, + physical = true, + is_visible = false, + collide_with_objects = true, + collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5}, + }, + + node = {}, + meta = {}, + floats = false, + + set_node = function(self, node, meta) + node.param2 = node.param2 or 0 + self.node = node + meta = meta or {} + if type(meta.to_table) == "function" then + meta = meta:to_table() + end + for _, list in pairs(meta.inventory or {}) do + for i, stack in pairs(list) do + if type(stack) == "userdata" then + list[i] = stack:to_string() + end + end + end + local def = core.registered_nodes[node.name] + if not def then + -- Don't allow unknown nodes to fall + core.log("info", + "Unknown falling node removed at ".. + core.pos_to_string(self.object:get_pos())) + self.object:remove() + return + end + self.meta = meta + + -- Cache whether we're supposed to float on water + self.floats = core.get_item_group(node.name, "float") ~= 0 + + -- Set entity visuals + if def.drawtype == "torchlike" or def.drawtype == "signlike" then + local textures + if def.tiles and def.tiles[1] then + local tile = def.tiles[1] + if type(tile) == "table" then + tile = tile.name + end + if def.drawtype == "torchlike" then + textures = { "("..tile..")^[transformFX", tile } + else + textures = { tile, "("..tile..")^[transformFX" } + end + end + local vsize + if def.visual_scale then + local s = def.visual_scale + vsize = vector.new(s, s, s) + end + self.object:set_properties({ + is_visible = true, + visual = "upright_sprite", + visual_size = vsize, + textures = textures, + glow = def.light_source, + }) + elseif def.drawtype ~= "airlike" then + local itemstring = node.name + if core.is_colored_paramtype(def.paramtype2) then + itemstring = core.itemstring_with_palette(itemstring, node.param2) + end + -- FIXME: solution needed for paramtype2 == "leveled" + -- Calculate size of falling node + local s = {} + s.x = (def.visual_scale or 1) * SCALE + s.y = s.x + s.z = s.x + -- Compensate for wield_scale + if def.wield_scale then + s.x = s.x / def.wield_scale.x + s.y = s.y / def.wield_scale.y + s.z = s.z / def.wield_scale.z + end + self.object:set_properties({ + is_visible = true, + wield_item = itemstring, + visual_size = s, + glow = def.light_source, + }) + end + + -- Set collision box (certain nodeboxes only for now) + local nb_types = {fixed=true, leveled=true, connected=true} + if def.drawtype == "nodebox" and def.node_box and + nb_types[def.node_box.type] and def.node_box.fixed then + local box = table.copy(def.node_box.fixed) + if type(box[1]) == "table" then + box = #box == 1 and box[1] or nil -- We can only use a single box + end + if box then + if def.paramtype2 == "leveled" and (self.node.level or 0) > 0 then + box[5] = -0.5 + self.node.level / 64 + end + self.object:set_properties({ + collisionbox = box + }) + end + end + + -- Rotate entity + if def.drawtype == "torchlike" then + self.object:set_yaw(math.pi*0.25) + elseif ((node.param2 ~= 0 or def.drawtype == "nodebox" or def.drawtype == "mesh") + and (def.wield_image == "" or def.wield_image == nil)) + or def.drawtype == "signlike" + or def.drawtype == "mesh" + or def.drawtype == "normal" + or def.drawtype == "nodebox" then + if (def.paramtype2 == "facedir" or def.paramtype2 == "colorfacedir") then + local fdir = node.param2 % 32 % 24 + -- Get rotation from a precalculated lookup table + local euler = facedir_to_euler[fdir + 1] + self.object:set_rotation(euler) + elseif (def.drawtype ~= "plantlike" and def.drawtype ~= "plantlike_rooted" and + (def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted" or def.drawtype == "signlike")) then + local rot = node.param2 % 8 + if (def.drawtype == "signlike" and def.paramtype2 ~= "wallmounted" and def.paramtype2 ~= "colorwallmounted") then + -- Change rotation to "floor" by default for non-wallmounted paramtype2 + rot = 1 + end + local pitch, yaw, roll = 0, 0, 0 + if def.drawtype == "nodebox" or def.drawtype == "mesh" then + if rot == 0 then + pitch, yaw = math.pi/2, 0 + elseif rot == 1 then + pitch, yaw = -math.pi/2, math.pi + elseif rot == 2 then + pitch, yaw = 0, math.pi/2 + elseif rot == 3 then + pitch, yaw = 0, -math.pi/2 + elseif rot == 4 then + pitch, yaw = 0, math.pi + end + else + if rot == 1 then + pitch, yaw = math.pi, math.pi + elseif rot == 2 then + pitch, yaw = math.pi/2, math.pi/2 + elseif rot == 3 then + pitch, yaw = math.pi/2, -math.pi/2 + elseif rot == 4 then + pitch, yaw = math.pi/2, math.pi + elseif rot == 5 then + pitch, yaw = math.pi/2, 0 + end + end + if def.drawtype == "signlike" then + pitch = pitch - math.pi/2 + if rot == 0 then + yaw = yaw + math.pi/2 + elseif rot == 1 then + yaw = yaw - math.pi/2 + end + elseif def.drawtype == "mesh" or def.drawtype == "normal" or def.drawtype == "nodebox" then + if rot >= 0 and rot <= 1 then + roll = roll + math.pi + else + yaw = yaw + math.pi + end + end + self.object:set_rotation({x=pitch, y=yaw, z=roll}) + elseif (def.drawtype == "mesh" and def.paramtype2 == "degrotate") then + local p2 = (node.param2 - (def.place_param2 or 0)) % 240 + local yaw = (p2 / 240) * (math.pi * 2) + self.object:set_yaw(yaw) + elseif (def.drawtype == "mesh" and def.paramtype2 == "colordegrotate") then + local p2 = (node.param2 % 32 - (def.place_param2 or 0) % 32) % 24 + local yaw = (p2 / 24) * (math.pi * 2) + self.object:set_yaw(yaw) + end + end + end, + + get_staticdata = function(self) + local ds = { + node = self.node, + meta = self.meta, + } + return core.serialize(ds) + end, + + on_activate = function(self, staticdata) + self.object:set_armor_groups({immortal = 1}) + self.object:set_acceleration(vector.new(0, -gravity, 0)) + + local ds = core.deserialize(staticdata) + if ds and ds.node then + self:set_node(ds.node, ds.meta) + elseif ds then + self:set_node(ds) + elseif staticdata ~= "" then + self:set_node({name = staticdata}) + end + end, + + try_place = function(self, bcp, bcn) + local bcd = core.registered_nodes[bcn.name] + -- Add levels if dropped on same leveled node + if bcd and bcd.paramtype2 == "leveled" and + bcn.name == self.node.name then + local addlevel = self.node.level + if (addlevel or 0) <= 0 then + addlevel = bcd.leveled + end + if core.add_node_level(bcp, addlevel) < addlevel then + return true + elseif bcd.buildable_to then + -- Node level has already reached max, don't place anything + return true + end + end + + -- Decide if we're replacing the node or placing on top + local np = vector.copy(bcp) + if bcd and bcd.buildable_to and + (not self.floats or bcd.liquidtype == "none") then + core.remove_node(bcp) + else + np.y = np.y + 1 + end + + -- Check what's here + local n2 = core.get_node(np) + local nd = core.registered_nodes[n2.name] + -- If it's not air or liquid, remove node and replace it with + -- it's drops + if n2.name ~= "air" and (not nd or nd.liquidtype == "none") then + if nd and nd.buildable_to == false then + nd.on_dig(np, n2, nil) + -- If it's still there, it might be protected + if core.get_node(np).name == n2.name then + return false + end + else + core.remove_node(np) + end + end + + -- Create node + local def = core.registered_nodes[self.node.name] + if def then + core.add_node(np, self.node) + if self.meta then + core.get_meta(np):from_table(self.meta) + end + if def.sounds and def.sounds.place then + core.sound_play(def.sounds.place, {pos = np}, true) + end + end + core.check_for_falling(np) + return true + end, + + on_step = function(self, dtime, moveresult) + -- Fallback code since collision detection can't tell us + -- about liquids (which do not collide) + if self.floats then + local pos = self.object:get_pos() + + local bcp = pos:offset(0, -0.7, 0):round() + local bcn = core.get_node(bcp) + + local bcd = core.registered_nodes[bcn.name] + if bcd and bcd.liquidtype ~= "none" then + if self:try_place(bcp, bcn) then + self.object:remove() + return + end + end + end + + assert(moveresult) + if not moveresult.collides then + return -- Nothing to do :) + end + + local bcp, bcn + local player_collision + if moveresult.touching_ground then + for _, info in ipairs(moveresult.collisions) do + if info.type == "object" then + if info.axis == "y" and info.object:is_player() then + player_collision = info + end + elseif info.axis == "y" then + bcp = info.node_pos + bcn = core.get_node(bcp) + break + end + end + end + + if not bcp then + -- We're colliding with something, but not the ground. Irrelevant to us. + if player_collision then + -- Continue falling through players by moving a little into + -- their collision box + -- TODO: this hack could be avoided in the future if objects + -- could choose who to collide with + local vel = self.object:get_velocity() + self.object:set_velocity(vector.new( + vel.x, + player_collision.old_velocity.y, + vel.z + )) + self.object:set_pos(self.object:get_pos():offset(0, -0.5, 0)) + end + return + elseif bcn.name == "ignore" then + -- Delete on contact with ignore at world edges + self.object:remove() + return + end + + local failure = false + + local pos = self.object:get_pos() + local distance = vector.apply(vector.subtract(pos, bcp), math.abs) + if distance.x >= 1 or distance.z >= 1 then + -- We're colliding with some part of a node that's sticking out + -- Since we don't want to visually teleport, drop as item + failure = true + elseif distance.y >= 2 then + -- Doors consist of a hidden top node and a bottom node that is + -- the actual door. Despite the top node being solid, the moveresult + -- almost always indicates collision with the bottom node. + -- Compensate for this by checking the top node + bcp.y = bcp.y + 1 + bcn = core.get_node(bcp) + local def = core.registered_nodes[bcn.name] + if not (def and def.walkable) then + failure = true -- This is unexpected, fail + end + end + + -- Try to actually place ourselves + if not failure then + failure = not self:try_place(bcp, bcn) + end + + if failure then + local drops = core.get_node_drops(self.node, "") + for _, item in pairs(drops) do + core.add_item(pos, item) + end + end + self.object:remove() + end +}) + +local function convert_to_falling_node(pos, node) + local obj = core.add_entity(pos, "__builtin:falling_node") + if not obj then + return false + end + -- remember node level, the entities' set_node() uses this + node.level = core.get_node_level(pos) + local meta = core.get_meta(pos) + local metatable = meta and meta:to_table() or {} + + local def = core.registered_nodes[node.name] + if def and def.sounds and def.sounds.fall then + core.sound_play(def.sounds.fall, {pos = pos}, true) + end + + obj:get_luaentity():set_node(node, metatable) + core.remove_node(pos) + return true, obj +end + +function core.spawn_falling_node(pos) + local node = core.get_node(pos) + if node.name == "air" or node.name == "ignore" then + return false + end + return convert_to_falling_node(pos, node) +end + +local function drop_attached_node(p) + local n = core.get_node(p) + local drops = core.get_node_drops(n, "") + local def = core.registered_items[n.name] + if def and def.preserve_metadata then + local oldmeta = core.get_meta(p):to_table().fields + -- Copy pos and node because the callback can modify them. + local pos_copy = vector.copy(p) + local node_copy = {name=n.name, param1=n.param1, param2=n.param2} + local drop_stacks = {} + for k, v in pairs(drops) do + drop_stacks[k] = ItemStack(v) + end + drops = drop_stacks + def.preserve_metadata(pos_copy, node_copy, oldmeta, drops) + end + if def and def.sounds and def.sounds.fall then + core.sound_play(def.sounds.fall, {pos = p}, true) + end + core.remove_node(p) + for _, item in pairs(drops) do + local pos = { + x = p.x + math.random()/2 - 0.25, + y = p.y + math.random()/2 - 0.25, + z = p.z + math.random()/2 - 0.25, + } + core.add_item(pos, item) + end +end + +function builtin_shared.check_attached_node(p, n) + local def = core.registered_nodes[n.name] + local d = vector.zero() + if def.paramtype2 == "wallmounted" or + def.paramtype2 == "colorwallmounted" then + -- The fallback vector here is in case 'wallmounted to dir' is nil due + -- to voxelmanip placing a wallmounted node without resetting a + -- pre-existing param2 value that is out-of-range for wallmounted. + -- The fallback vector corresponds to param2 = 0. + d = core.wallmounted_to_dir(n.param2) or vector.new(0, 1, 0) + else + d.y = -1 + end + local p2 = vector.add(p, d) + local nn = core.get_node(p2).name + local def2 = core.registered_nodes[nn] + if def2 and not def2.walkable then + return false + end + return true +end + +-- +-- Some common functions +-- + +function core.check_single_for_falling(p) + local n = core.get_node(p) + if core.get_item_group(n.name, "falling_node") ~= 0 then + local p_bottom = vector.offset(p, 0, -1, 0) + -- Only spawn falling node if node below is loaded + local n_bottom = core.get_node_or_nil(p_bottom) + local d_bottom = n_bottom and core.registered_nodes[n_bottom.name] + if d_bottom then + local same = n.name == n_bottom.name + -- Let leveled nodes fall if it can merge with the bottom node + if same and d_bottom.paramtype2 == "leveled" and + core.get_node_level(p_bottom) < + core.get_node_max_level(p_bottom) then + convert_to_falling_node(p, n) + return true + end + -- Otherwise only if the bottom node is considered "fall through" + if not same and + (not d_bottom.walkable or d_bottom.buildable_to) and + (core.get_item_group(n.name, "float") == 0 or + d_bottom.liquidtype == "none") then + convert_to_falling_node(p, n) + return true + end + end + end + + if core.get_item_group(n.name, "attached_node") ~= 0 then + if not builtin_shared.check_attached_node(p, n) then + drop_attached_node(p) + return true + end + end + + return false +end + +-- This table is specifically ordered. +-- We don't walk diagonals, only our direct neighbors, and self. +-- Down first as likely case, but always before self. The same with sides. +-- Up must come last, so that things above self will also fall all at once. +local check_for_falling_neighbors = { + vector.new(-1, -1, 0), + vector.new( 1, -1, 0), + vector.new( 0, -1, -1), + vector.new( 0, -1, 1), + vector.new( 0, -1, 0), + vector.new(-1, 0, 0), + vector.new( 1, 0, 0), + vector.new( 0, 0, 1), + vector.new( 0, 0, -1), + vector.new( 0, 0, 0), + vector.new( 0, 1, 0), +} + +function core.check_for_falling(p) + -- Round p to prevent falling entities to get stuck. + p = vector.round(p) + + -- We make a stack, and manually maintain size for performance. + -- Stored in the stack, we will maintain tables with pos, and + -- last neighbor visited. This way, when we get back to each + -- node, we know which directions we have already walked, and + -- which direction is the next to walk. + local s = {} + local n = 0 + -- The neighbor order we will visit from our table. + local v = 1 + + while true do + -- Push current pos onto the stack. + n = n + 1 + s[n] = {p = p, v = v} + -- Select next node from neighbor list. + p = vector.add(p, check_for_falling_neighbors[v]) + -- Now we check out the node. If it is in need of an update, + -- it will let us know in the return value (true = updated). + if not core.check_single_for_falling(p) then + -- If we don't need to "recurse" (walk) to it then pop + -- our previous pos off the stack and continue from there, + -- with the v value we were at when we last were at that + -- node + repeat + local pop = s[n] + p = pop.p + v = pop.v + s[n] = nil + n = n - 1 + -- If there's nothing left on the stack, and no + -- more sides to walk to, we're done and can exit + if n == 0 and v == 11 then + return + end + until v < 11 + -- The next round walk the next neighbor in list. + v = v + 1 + else + -- If we did need to walk the neighbor, then + -- start walking it from the walk order start (1), + -- and not the order we just pushed up the stack. + v = 1 + end + end +end + +-- +-- Global callbacks +-- + +local function on_placenode(p, node) + core.check_for_falling(p) +end +core.register_on_placenode(on_placenode) + +local function on_dignode(p, node) + core.check_for_falling(p) +end +core.register_on_dignode(on_dignode) + +local function on_punchnode(p, node) + core.check_for_falling(p) +end +core.register_on_punchnode(on_punchnode) diff --git a/builtin/game/features.lua b/builtin/game/features.lua new file mode 100644 index 0000000..73b1636 --- /dev/null +++ b/builtin/game/features.lua @@ -0,0 +1,46 @@ +-- Minetest: builtin/features.lua + +core.features = { + glasslike_framed = true, + nodebox_as_selectionbox = true, + get_all_craft_recipes_works = true, + use_texture_alpha = true, + no_legacy_abms = true, + texture_names_parens = true, + area_store_custom_ids = true, + add_entity_with_staticdata = true, + no_chat_message_prediction = true, + object_use_texture_alpha = true, + object_independent_selectionbox = true, + httpfetch_binary_data = true, + formspec_version_element = true, + area_store_persistent_ids = true, + pathfinder_works = true, + object_step_has_moveresult = true, + direct_velocity_on_players = true, + use_texture_alpha_string_modes = true, + degrotate_240_steps = true, + abm_min_max_y = true, + particlespawner_tweenable = true, + dynamic_add_media_table = true, + get_sky_as_table = true, +} + +function core.has_feature(arg) + if type(arg) == "table" then + local missing_features = {} + local result = true + for ftr in pairs(arg) do + if not core.features[ftr] then + missing_features[ftr] = true + result = false + end + end + return result, missing_features + elseif type(arg) == "string" then + if not core.features[arg] then + return false, {[arg]=true} + end + return true, {} + end +end diff --git a/builtin/game/forceloading.lua b/builtin/game/forceloading.lua new file mode 100644 index 0000000..8043e5d --- /dev/null +++ b/builtin/game/forceloading.lua @@ -0,0 +1,126 @@ +-- Prevent anyone else accessing those functions +local forceload_block = core.forceload_block +local forceload_free_block = core.forceload_free_block +core.forceload_block = nil +core.forceload_free_block = nil + +local blocks_forceloaded +local blocks_temploaded = {} +local total_forceloaded = 0 + +-- true, if the forceloaded blocks got changed (flag for persistence on-disk) +local forceload_blocks_changed = false + +local BLOCKSIZE = core.MAP_BLOCKSIZE +local function get_blockpos(pos) + return { + x = math.floor(pos.x/BLOCKSIZE), + y = math.floor(pos.y/BLOCKSIZE), + z = math.floor(pos.z/BLOCKSIZE)} +end + +-- When we create/free a forceload, it's either transient or persistent. We want +-- to add to/remove from the table that corresponds to the type of forceload, but +-- we also need the other table because whether we forceload a block depends on +-- both tables. +-- This function returns the "primary" table we are adding to/removing from, and +-- the other table. +local function get_relevant_tables(transient) + if transient then + return blocks_temploaded, blocks_forceloaded + else + return blocks_forceloaded, blocks_temploaded + end +end + +function core.forceload_block(pos, transient) + -- set changed flag + forceload_blocks_changed = true + + local blockpos = get_blockpos(pos) + local hash = core.hash_node_position(blockpos) + local relevant_table, other_table = get_relevant_tables(transient) + if relevant_table[hash] ~= nil then + relevant_table[hash] = relevant_table[hash] + 1 + return true + elseif other_table[hash] ~= nil then + relevant_table[hash] = 1 + else + if total_forceloaded >= (tonumber(core.settings:get("max_forceloaded_blocks")) or 16) then + return false + end + total_forceloaded = total_forceloaded+1 + relevant_table[hash] = 1 + forceload_block(blockpos) + return true + end +end + +function core.forceload_free_block(pos, transient) + -- set changed flag + forceload_blocks_changed = true + + local blockpos = get_blockpos(pos) + local hash = core.hash_node_position(blockpos) + local relevant_table, other_table = get_relevant_tables(transient) + if relevant_table[hash] == nil then return end + if relevant_table[hash] > 1 then + relevant_table[hash] = relevant_table[hash] - 1 + elseif other_table[hash] ~= nil then + relevant_table[hash] = nil + else + total_forceloaded = total_forceloaded-1 + relevant_table[hash] = nil + forceload_free_block(blockpos) + end +end + +-- Keep the forceloaded areas after restart +local wpath = core.get_worldpath() +local function read_file(filename) + local f = io.open(filename, "r") + if f==nil then return {} end + local t = f:read("*all") + f:close() + if t=="" or t==nil then return {} end + return core.deserialize(t) or {} +end + +blocks_forceloaded = read_file(wpath.."/force_loaded.txt") +for _, __ in pairs(blocks_forceloaded) do + total_forceloaded = total_forceloaded + 1 +end + +core.after(5, function() + for hash, _ in pairs(blocks_forceloaded) do + local blockpos = core.get_position_from_hash(hash) + forceload_block(blockpos) + end +end) + +-- persists the currently forceloaded blocks to disk +local function persist_forceloaded_blocks() + local data = core.serialize(blocks_forceloaded) + core.safe_file_write(wpath.."/force_loaded.txt", data) +end + +-- periodical forceload persistence +local function periodically_persist_forceloaded_blocks() + + -- only persist if the blocks actually changed + if forceload_blocks_changed then + persist_forceloaded_blocks() + + -- reset changed flag + forceload_blocks_changed = false + end + + -- recheck after some time + core.after(10, periodically_persist_forceloaded_blocks) +end + +-- persist periodically +core.after(5, periodically_persist_forceloaded_blocks) + +-- persist on shutdown +core.register_on_shutdown(persist_forceloaded_blocks) diff --git a/builtin/game/init.lua b/builtin/game/init.lua new file mode 100644 index 0000000..d7606f3 --- /dev/null +++ b/builtin/game/init.lua @@ -0,0 +1,40 @@ + +local scriptpath = core.get_builtin_path() +local commonpath = scriptpath .. "common" .. DIR_DELIM +local gamepath = scriptpath .. "game".. DIR_DELIM + +-- Shared between builtin files, but +-- not exposed to outer context +local builtin_shared = {} + +dofile(gamepath .. "constants.lua") +dofile(gamepath .. "item_s.lua") +assert(loadfile(gamepath .. "item.lua"))(builtin_shared) +dofile(gamepath .. "register.lua") + +if core.settings:get_bool("profiler.load") then + profiler = dofile(scriptpath .. "profiler" .. DIR_DELIM .. "init.lua") +end + +dofile(commonpath .. "after.lua") +dofile(commonpath .. "mod_storage.lua") +dofile(gamepath .. "item_entity.lua") +dofile(gamepath .. "deprecated.lua") +dofile(gamepath .. "misc_s.lua") +dofile(gamepath .. "misc.lua") +dofile(gamepath .. "privileges.lua") +dofile(gamepath .. "auth.lua") +dofile(commonpath .. "chatcommands.lua") +dofile(gamepath .. "chat.lua") +dofile(commonpath .. "information_formspecs.lua") +dofile(gamepath .. "static_spawn.lua") +dofile(gamepath .. "detached_inventory.lua") +assert(loadfile(gamepath .. "falling.lua"))(builtin_shared) +dofile(gamepath .. "features.lua") +dofile(gamepath .. "voxelarea.lua") +dofile(gamepath .. "forceloading.lua") +dofile(gamepath .. "statbars.lua") +dofile(gamepath .. "knockback.lua") +dofile(gamepath .. "async.lua") + +profiler = nil diff --git a/builtin/game/item.lua b/builtin/game/item.lua new file mode 100644 index 0000000..00601c6 --- /dev/null +++ b/builtin/game/item.lua @@ -0,0 +1,680 @@ +-- Minetest: builtin/item.lua + +local builtin_shared = ... + +local function copy_pointed_thing(pointed_thing) + return { + type = pointed_thing.type, + above = pointed_thing.above and vector.copy(pointed_thing.above), + under = pointed_thing.under and vector.copy(pointed_thing.under), + ref = pointed_thing.ref, + } +end + +-- +-- Item definition helpers +-- + +function core.get_pointed_thing_position(pointed_thing, above) + if pointed_thing.type == "node" then + if above then + -- The position where a node would be placed + return pointed_thing.above + end + -- The position where a node would be dug + return pointed_thing.under + elseif pointed_thing.type == "object" then + return pointed_thing.ref and pointed_thing.ref:get_pos() + end +end + +local function has_all_groups(tbl, required_groups) + if type(required_groups) == "string" then + return (tbl[required_groups] or 0) ~= 0 + end + for _, group in ipairs(required_groups) do + if (tbl[group] or 0) == 0 then + return false + end + end + return true +end + +function core.get_node_drops(node, toolname) + -- Compatibility, if node is string + local nodename = node + local param2 = 0 + -- New format, if node is table + if (type(node) == "table") then + nodename = node.name + param2 = node.param2 + end + local def = core.registered_nodes[nodename] + local drop = def and def.drop + local ptype = def and def.paramtype2 + -- get color, if there is color (otherwise nil) + local palette_index = core.strip_param2_color(param2, ptype) + if drop == nil then + -- default drop + if palette_index then + local stack = ItemStack(nodename) + stack:get_meta():set_int("palette_index", palette_index) + return {stack:to_string()} + end + return {nodename} + elseif type(drop) == "string" then + -- itemstring drop + return drop ~= "" and {drop} or {} + elseif drop.items == nil then + -- drop = {} to disable default drop + return {} + end + + -- Extended drop table + local got_items = {} + local got_count = 0 + for _, item in ipairs(drop.items) do + local good_rarity = true + local good_tool = true + if item.rarity ~= nil then + good_rarity = item.rarity < 1 or math.random(item.rarity) == 1 + end + if item.tools ~= nil or item.tool_groups ~= nil then + good_tool = false + end + if item.tools ~= nil and toolname then + for _, tool in ipairs(item.tools) do + if tool:sub(1, 1) == '~' then + good_tool = toolname:find(tool:sub(2)) ~= nil + else + good_tool = toolname == tool + end + if good_tool then + break + end + end + end + if item.tool_groups ~= nil and toolname then + local tooldef = core.registered_items[toolname] + if tooldef ~= nil and type(tooldef.groups) == "table" then + if type(item.tool_groups) == "string" then + -- tool_groups can be a string which specifies the required group + good_tool = core.get_item_group(toolname, item.tool_groups) ~= 0 + else + -- tool_groups can be a list of sufficient requirements. + -- i.e. if any item in the list can be satisfied then the tool is good + assert(type(item.tool_groups) == "table") + for _, required_groups in ipairs(item.tool_groups) do + -- required_groups can be either a string (a single group), + -- or an array of strings where all must be in tooldef.groups + good_tool = has_all_groups(tooldef.groups, required_groups) + if good_tool then + break + end + end + end + end + end + if good_rarity and good_tool then + got_count = got_count + 1 + for _, add_item in ipairs(item.items) do + -- add color, if necessary + if item.inherit_color and palette_index then + local stack = ItemStack(add_item) + stack:get_meta():set_int("palette_index", palette_index) + add_item = stack:to_string() + end + got_items[#got_items+1] = add_item + end + if drop.max_items ~= nil and got_count == drop.max_items then + break + end + end + end + return got_items +end + +local function user_name(user) + return user and user:get_player_name() or "" +end + +-- Returns a logging function. For empty names, does not log. +local function make_log(name) + return name ~= "" and core.log or function() end +end + +function core.item_place_node(itemstack, placer, pointed_thing, param2, + prevent_after_place) + local def = itemstack:get_definition() + if def.type ~= "node" or pointed_thing.type ~= "node" then + return itemstack, nil + end + + local under = pointed_thing.under + local oldnode_under = core.get_node_or_nil(under) + local above = pointed_thing.above + local oldnode_above = core.get_node_or_nil(above) + local playername = user_name(placer) + local log = make_log(playername) + + if not oldnode_under or not oldnode_above then + log("info", playername .. " tried to place" + .. " node in unloaded position " .. core.pos_to_string(above)) + return itemstack, nil + end + + local olddef_under = core.registered_nodes[oldnode_under.name] + olddef_under = olddef_under or core.nodedef_default + local olddef_above = core.registered_nodes[oldnode_above.name] + olddef_above = olddef_above or core.nodedef_default + + if not olddef_above.buildable_to and not olddef_under.buildable_to then + log("info", playername .. " tried to place" + .. " node in invalid position " .. core.pos_to_string(above) + .. ", replacing " .. oldnode_above.name) + return itemstack, nil + end + + -- Place above pointed node + local place_to = vector.copy(above) + + -- If node under is buildable_to, place into it instead (eg. snow) + if olddef_under.buildable_to then + log("info", "node under is buildable to") + place_to = vector.copy(under) + end + + if core.is_protected(place_to, playername) then + log("action", playername + .. " tried to place " .. def.name + .. " at protected position " + .. core.pos_to_string(place_to)) + core.record_protection_violation(place_to, playername) + return itemstack, nil + end + + local oldnode = core.get_node(place_to) + local newnode = {name = def.name, param1 = 0, param2 = param2 or 0} + + -- Calculate direction for wall mounted stuff like torches and signs + if def.place_param2 ~= nil then + newnode.param2 = def.place_param2 + elseif (def.paramtype2 == "wallmounted" or + def.paramtype2 == "colorwallmounted") and not param2 then + local dir = vector.subtract(under, above) + newnode.param2 = core.dir_to_wallmounted(dir) + -- Calculate the direction for furnaces and chests and stuff + elseif (def.paramtype2 == "facedir" or + def.paramtype2 == "colorfacedir") and not param2 then + local placer_pos = placer and placer:get_pos() + if placer_pos then + local dir = vector.subtract(above, placer_pos) + newnode.param2 = core.dir_to_facedir(dir) + log("info", "facedir: " .. newnode.param2) + end + end + + local metatable = itemstack:get_meta():to_table().fields + + -- Transfer color information + if metatable.palette_index and not def.place_param2 then + local color_divisor = nil + if def.paramtype2 == "color" then + color_divisor = 1 + elseif def.paramtype2 == "colorwallmounted" then + color_divisor = 8 + elseif def.paramtype2 == "colorfacedir" then + color_divisor = 32 + elseif def.paramtype2 == "colordegrotate" then + color_divisor = 32 + end + if color_divisor then + local color = math.floor(metatable.palette_index / color_divisor) + local other = newnode.param2 % color_divisor + newnode.param2 = color * color_divisor + other + end + end + + -- Check if the node is attached and if it can be placed there + if core.get_item_group(def.name, "attached_node") ~= 0 and + not builtin_shared.check_attached_node(place_to, newnode) then + log("action", "attached node " .. def.name .. + " can not be placed at " .. core.pos_to_string(place_to)) + return itemstack, nil + end + + log("action", playername .. " places node " + .. def.name .. " at " .. core.pos_to_string(place_to)) + + -- Add node and update + core.add_node(place_to, newnode) + + -- Play sound if it was done by a player + if playername ~= "" and def.sounds and def.sounds.place then + core.sound_play(def.sounds.place, { + pos = place_to, + exclude_player = playername, + }, true) + end + + local take_item = true + + -- Run callback + if def.after_place_node and not prevent_after_place then + -- Deepcopy place_to and pointed_thing because callback can modify it + local place_to_copy = vector.copy(place_to) + local pointed_thing_copy = copy_pointed_thing(pointed_thing) + if def.after_place_node(place_to_copy, placer, itemstack, + pointed_thing_copy) then + take_item = false + end + end + + -- Run script hook + for _, callback in ipairs(core.registered_on_placenodes) do + -- Deepcopy pos, node and pointed_thing because callback can modify them + local place_to_copy = vector.copy(place_to) + local newnode_copy = {name=newnode.name, param1=newnode.param1, param2=newnode.param2} + local oldnode_copy = {name=oldnode.name, param1=oldnode.param1, param2=oldnode.param2} + local pointed_thing_copy = copy_pointed_thing(pointed_thing) + if callback(place_to_copy, newnode_copy, placer, oldnode_copy, itemstack, pointed_thing_copy) then + take_item = false + end + end + + if take_item then + itemstack:take_item() + end + return itemstack, place_to +end + +-- deprecated, item_place does not call this +function core.item_place_object(itemstack, placer, pointed_thing) + local pos = core.get_pointed_thing_position(pointed_thing, true) + if pos ~= nil then + local item = itemstack:take_item() + core.add_item(pos, item) + end + return itemstack +end + +function core.item_place(itemstack, placer, pointed_thing, param2) + -- Call on_rightclick if the pointed node defines it + if pointed_thing.type == "node" and placer and + not placer:get_player_control().sneak then + local n = core.get_node(pointed_thing.under) + local nn = n.name + if core.registered_nodes[nn] and core.registered_nodes[nn].on_rightclick then + return core.registered_nodes[nn].on_rightclick(pointed_thing.under, n, + placer, itemstack, pointed_thing) or itemstack, nil + end + end + + -- Place if node, otherwise do nothing + if itemstack:get_definition().type == "node" then + return core.item_place_node(itemstack, placer, pointed_thing, param2) + end + return itemstack, nil +end + +function core.item_secondary_use(itemstack, placer) + return itemstack +end + +function core.item_drop(itemstack, dropper, pos) + local dropper_is_player = dropper and dropper:is_player() + local p = table.copy(pos) + local cnt = itemstack:get_count() + if dropper_is_player then + p.y = p.y + 1.2 + end + local item = itemstack:take_item(cnt) + local obj = core.add_item(p, item) + if obj then + if dropper_is_player then + local dir = dropper:get_look_dir() + dir.x = dir.x * 2.9 + dir.y = dir.y * 2.9 + 2 + dir.z = dir.z * 2.9 + obj:set_velocity(dir) + obj:get_luaentity().dropped_by = dropper:get_player_name() + end + return itemstack + end + -- If we reach this, adding the object to the + -- environment failed +end + +function core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing) + for _, callback in pairs(core.registered_on_item_eats) do + local result = callback(hp_change, replace_with_item, itemstack, user, pointed_thing) + if result then + return result + end + end + -- read definition before potentially emptying the stack + local def = itemstack:get_definition() + if itemstack:take_item():is_empty() then + return itemstack + end + + if def and def.sound and def.sound.eat then + core.sound_play(def.sound.eat, { + pos = user:get_pos(), + max_hear_distance = 16 + }, true) + end + + -- Changing hp might kill the player causing mods to do who-knows-what to the + -- inventory, so do this before set_hp(). + if replace_with_item then + if itemstack:is_empty() then + itemstack:add_item(replace_with_item) + else + local inv = user:get_inventory() + -- Check if inv is null, since non-players don't have one + if inv and inv:room_for_item("main", {name=replace_with_item}) then + inv:add_item("main", replace_with_item) + else + local pos = user:get_pos() + pos.y = math.floor(pos.y + 0.5) + core.add_item(pos, replace_with_item) + end + end + end + user:set_wielded_item(itemstack) + + user:set_hp(user:get_hp() + hp_change) + + return nil -- don't overwrite wield item a second time +end + +function core.item_eat(hp_change, replace_with_item) + return function(itemstack, user, pointed_thing) -- closure + if user then + return core.do_item_eat(hp_change, replace_with_item, itemstack, user, pointed_thing) + end + end +end + +function core.node_punch(pos, node, puncher, pointed_thing) + -- Run script hook + for _, callback in ipairs(core.registered_on_punchnodes) do + -- Copy pos and node because callback can modify them + local pos_copy = vector.copy(pos) + local node_copy = {name=node.name, param1=node.param1, param2=node.param2} + local pointed_thing_copy = pointed_thing and copy_pointed_thing(pointed_thing) or nil + callback(pos_copy, node_copy, puncher, pointed_thing_copy) + end +end + +function core.handle_node_drops(pos, drops, digger) + -- Add dropped items to object's inventory + local inv = digger and digger:get_inventory() + local give_item + if inv then + give_item = function(item) + return inv:add_item("main", item) + end + else + give_item = function(item) + -- itemstring to ItemStack for left:is_empty() + return ItemStack(item) + end + end + + for _, dropped_item in pairs(drops) do + local left = give_item(dropped_item) + if not left:is_empty() then + local p = vector.offset(pos, + math.random()/2-0.25, + math.random()/2-0.25, + math.random()/2-0.25 + ) + core.add_item(p, left) + end + end +end + +function core.node_dig(pos, node, digger) + local diggername = user_name(digger) + local log = make_log(diggername) + local def = core.registered_nodes[node.name] + -- Copy pos because the callback could modify it + if def and (not def.diggable or + (def.can_dig and not def.can_dig(vector.copy(pos), digger))) then + log("info", diggername .. " tried to dig " + .. node.name .. " which is not diggable " + .. core.pos_to_string(pos)) + return false + end + + if core.is_protected(pos, diggername) then + log("action", diggername + .. " tried to dig " .. node.name + .. " at protected position " + .. core.pos_to_string(pos)) + core.record_protection_violation(pos, diggername) + return false + end + + log('action', diggername .. " digs " + .. node.name .. " at " .. core.pos_to_string(pos)) + + local wielded = digger and digger:get_wielded_item() + local drops = core.get_node_drops(node, wielded and wielded:get_name()) + + if wielded then + local wdef = wielded:get_definition() + local tp = wielded:get_tool_capabilities() + local dp = core.get_dig_params(def and def.groups, tp, wielded:get_wear()) + if wdef and wdef.after_use then + wielded = wdef.after_use(wielded, digger, node, dp) or wielded + else + -- Wear out tool + if not core.is_creative_enabled(diggername) then + wielded:add_wear(dp.wear) + if wielded:get_count() == 0 and wdef.sound and wdef.sound.breaks then + core.sound_play(wdef.sound.breaks, { + pos = pos, + gain = 0.5 + }, true) + end + end + end + digger:set_wielded_item(wielded) + end + + -- Check to see if metadata should be preserved. + if def and def.preserve_metadata then + local oldmeta = core.get_meta(pos):to_table().fields + -- Copy pos and node because the callback can modify them. + local pos_copy = vector.copy(pos) + local node_copy = {name=node.name, param1=node.param1, param2=node.param2} + local drop_stacks = {} + for k, v in pairs(drops) do + drop_stacks[k] = ItemStack(v) + end + drops = drop_stacks + def.preserve_metadata(pos_copy, node_copy, oldmeta, drops) + end + + -- Handle drops + core.handle_node_drops(pos, drops, digger) + + local oldmetadata = nil + if def and def.after_dig_node then + oldmetadata = core.get_meta(pos):to_table() + end + + -- Remove node and update + core.remove_node(pos) + + -- Play sound if it was done by a player + if diggername ~= "" and def and def.sounds and def.sounds.dug then + core.sound_play(def.sounds.dug, { + pos = pos, + exclude_player = diggername, + }, true) + end + + -- Run callback + if def and def.after_dig_node then + -- Copy pos and node because callback can modify them + local pos_copy = vector.copy(pos) + local node_copy = {name=node.name, param1=node.param1, param2=node.param2} + def.after_dig_node(pos_copy, node_copy, oldmetadata, digger) + end + + -- Run script hook + for _, callback in ipairs(core.registered_on_dignodes) do + local origin = core.callback_origins[callback] + core.set_last_run_mod(origin.mod) + + -- Copy pos and node because callback can modify them + local pos_copy = vector.copy(pos) + local node_copy = {name=node.name, param1=node.param1, param2=node.param2} + callback(pos_copy, node_copy, digger) + end + + return true +end + +function core.itemstring_with_palette(item, palette_index) + local stack = ItemStack(item) -- convert to ItemStack + stack:get_meta():set_int("palette_index", palette_index) + return stack:to_string() +end + +function core.itemstring_with_color(item, colorstring) + local stack = ItemStack(item) -- convert to ItemStack + stack:get_meta():set_string("color", colorstring) + return stack:to_string() +end + +-- This is used to allow mods to redefine core.item_place and so on +-- NOTE: This is not the preferred way. Preferred way is to provide enough +-- callbacks to not require redefining global functions. -celeron55 +local function redef_wrapper(table, name) + return function(...) + return table[name](...) + end +end + +-- +-- Item definition defaults +-- + +local default_stack_max = tonumber(core.settings:get("default_stack_max")) or 99 + +core.nodedef_default = { + -- Item properties + type="node", + -- name intentionally not defined here + description = "", + groups = {}, + inventory_image = "", + wield_image = "", + wield_scale = vector.new(1, 1, 1), + stack_max = default_stack_max, + usable = false, + liquids_pointable = false, + tool_capabilities = nil, + node_placement_prediction = nil, + + -- Interaction callbacks + on_place = redef_wrapper(core, 'item_place'), -- core.item_place + on_drop = redef_wrapper(core, 'item_drop'), -- core.item_drop + on_use = nil, + can_dig = nil, + + on_punch = redef_wrapper(core, 'node_punch'), -- core.node_punch + on_rightclick = nil, + on_dig = redef_wrapper(core, 'node_dig'), -- core.node_dig + + on_receive_fields = nil, + + -- Node properties + drawtype = "normal", + visual_scale = 1.0, + tiles = nil, + special_tiles = nil, + post_effect_color = {a=0, r=0, g=0, b=0}, + paramtype = "none", + paramtype2 = "none", + is_ground_content = true, + sunlight_propagates = false, + walkable = true, + pointable = true, + diggable = true, + climbable = false, + buildable_to = false, + floodable = false, + liquidtype = "none", + liquid_alternative_flowing = "", + liquid_alternative_source = "", + liquid_viscosity = 0, + drowning = 0, + light_source = 0, + damage_per_second = 0, + selection_box = {type="regular"}, + legacy_facedir_simple = false, + legacy_wallmounted = false, +} + +core.craftitemdef_default = { + type="craft", + -- name intentionally not defined here + description = "", + groups = {}, + inventory_image = "", + wield_image = "", + wield_scale = vector.new(1, 1, 1), + stack_max = default_stack_max, + liquids_pointable = false, + tool_capabilities = nil, + + -- Interaction callbacks + on_place = redef_wrapper(core, 'item_place'), -- core.item_place + on_drop = redef_wrapper(core, 'item_drop'), -- core.item_drop + on_secondary_use = redef_wrapper(core, 'item_secondary_use'), + on_use = nil, +} + +core.tooldef_default = { + type="tool", + -- name intentionally not defined here + description = "", + groups = {}, + inventory_image = "", + wield_image = "", + wield_scale = vector.new(1, 1, 1), + stack_max = 1, + liquids_pointable = false, + tool_capabilities = nil, + + -- Interaction callbacks + on_place = redef_wrapper(core, 'item_place'), -- core.item_place + on_secondary_use = redef_wrapper(core, 'item_secondary_use'), + on_drop = redef_wrapper(core, 'item_drop'), -- core.item_drop + on_use = nil, +} + +core.noneitemdef_default = { -- This is used for the hand and unknown items + type="none", + -- name intentionally not defined here + description = "", + groups = {}, + inventory_image = "", + wield_image = "", + wield_scale = vector.new(1, 1, 1), + stack_max = default_stack_max, + liquids_pointable = false, + tool_capabilities = nil, + + -- Interaction callbacks + on_place = redef_wrapper(core, 'item_place'), + on_secondary_use = redef_wrapper(core, 'item_secondary_use'), + on_drop = nil, + on_use = nil, +} diff --git a/builtin/game/item_entity.lua b/builtin/game/item_entity.lua new file mode 100644 index 0000000..53f98a7 --- /dev/null +++ b/builtin/game/item_entity.lua @@ -0,0 +1,333 @@ +-- Minetest: builtin/item_entity.lua + +function core.spawn_item(pos, item) + -- Take item in any format + local stack = ItemStack(item) + local obj = core.add_entity(pos, "__builtin:item") + -- Don't use obj if it couldn't be added to the map. + if obj then + obj:get_luaentity():set_item(stack:to_string()) + end + return obj +end + +-- If item_entity_ttl is not set, enity will have default life time +-- Setting it to -1 disables the feature + +local time_to_live = tonumber(core.settings:get("item_entity_ttl")) or 900 +local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81 + + +core.register_entity(":__builtin:item", { + initial_properties = { + hp_max = 1, + physical = true, + collide_with_objects = false, + collisionbox = {-0.3, -0.3, -0.3, 0.3, 0.3, 0.3}, + visual = "wielditem", + visual_size = {x = 0.4, y = 0.4}, + textures = {""}, + is_visible = false, + }, + + itemstring = "", + moving_state = true, + physical_state = true, + -- Item expiry + age = 0, + -- Pushing item out of solid nodes + force_out = nil, + force_out_start = nil, + + set_item = function(self, item) + local stack = ItemStack(item or self.itemstring) + self.itemstring = stack:to_string() + if self.itemstring == "" then + -- item not yet known + return + end + + -- Backwards compatibility: old clients use the texture + -- to get the type of the item + local itemname = stack:is_known() and stack:get_name() or "unknown" + + local max_count = stack:get_stack_max() + local count = math.min(stack:get_count(), max_count) + local size = 0.2 + 0.1 * (count / max_count) ^ (1 / 3) + local def = core.registered_items[itemname] + local glow = def and def.light_source and + math.floor(def.light_source / 2 + 0.5) + + local size_bias = 1e-3 * math.random() -- small random bias to counter Z-fighting + local c = {-size, -size, -size, size, size, size} + self.object:set_properties({ + is_visible = true, + visual = "wielditem", + textures = {itemname}, + visual_size = {x = size + size_bias, y = size + size_bias}, + collisionbox = c, + automatic_rotate = math.pi * 0.5 * 0.2 / size, + wield_item = self.itemstring, + glow = glow, + }) + + -- cache for usage in on_step + self._collisionbox = c + end, + + get_staticdata = function(self) + return core.serialize({ + itemstring = self.itemstring, + age = self.age, + dropped_by = self.dropped_by + }) + end, + + on_activate = function(self, staticdata, dtime_s) + if string.sub(staticdata, 1, string.len("return")) == "return" then + local data = core.deserialize(staticdata) + if data and type(data) == "table" then + self.itemstring = data.itemstring + self.age = (data.age or 0) + dtime_s + self.dropped_by = data.dropped_by + end + else + self.itemstring = staticdata + end + self.object:set_armor_groups({immortal = 1}) + self.object:set_velocity({x = 0, y = 2, z = 0}) + self.object:set_acceleration({x = 0, y = -gravity, z = 0}) + self._collisionbox = self.initial_properties.collisionbox + self:set_item() + end, + + try_merge_with = function(self, own_stack, object, entity) + if self.age == entity.age then + -- Can not merge with itself + return false + end + + local stack = ItemStack(entity.itemstring) + local name = stack:get_name() + if own_stack:get_name() ~= name or + own_stack:get_meta() ~= stack:get_meta() or + own_stack:get_wear() ~= stack:get_wear() or + own_stack:get_free_space() == 0 then + -- Can not merge different or full stack + return false + end + + local count = own_stack:get_count() + local total_count = stack:get_count() + count + local max_count = stack:get_stack_max() + + if total_count > max_count then + return false + end + -- Merge the remote stack into this one + + local pos = object:get_pos() + pos.y = pos.y + ((total_count - count) / max_count) * 0.15 + self.object:move_to(pos) + + self.age = 0 -- Handle as new entity + own_stack:set_count(total_count) + self:set_item(own_stack) + + entity.itemstring = "" + object:remove() + return true + end, + + enable_physics = function(self) + if not self.physical_state then + self.physical_state = true + self.object:set_properties({physical = true}) + self.object:set_velocity({x=0, y=0, z=0}) + self.object:set_acceleration({x=0, y=-gravity, z=0}) + end + end, + + disable_physics = function(self) + if self.physical_state then + self.physical_state = false + self.object:set_properties({physical = false}) + self.object:set_velocity({x=0, y=0, z=0}) + self.object:set_acceleration({x=0, y=0, z=0}) + end + end, + + on_step = function(self, dtime, moveresult) + self.age = self.age + dtime + if time_to_live > 0 and self.age > time_to_live then + self.itemstring = "" + self.object:remove() + return + end + + local pos = self.object:get_pos() + local node = core.get_node_or_nil({ + x = pos.x, + y = pos.y + self._collisionbox[2] - 0.05, + z = pos.z + }) + -- Delete in 'ignore' nodes + if node and node.name == "ignore" then + self.itemstring = "" + self.object:remove() + return + end + + if self.force_out then + -- This code runs after the entity got a push from the is_stuck code. + -- It makes sure the entity is entirely outside the solid node + local c = self._collisionbox + local s = self.force_out_start + local f = self.force_out + local ok = (f.x > 0 and pos.x + c[1] > s.x + 0.5) or + (f.y > 0 and pos.y + c[2] > s.y + 0.5) or + (f.z > 0 and pos.z + c[3] > s.z + 0.5) or + (f.x < 0 and pos.x + c[4] < s.x - 0.5) or + (f.z < 0 and pos.z + c[6] < s.z - 0.5) + if ok then + -- Item was successfully forced out + self.force_out = nil + self:enable_physics() + return + end + end + + if not self.physical_state then + return -- Don't do anything + end + + assert(moveresult, + "Collision info missing, this is caused by an out-of-date/buggy mod or game") + + if not moveresult.collides then + -- future TODO: items should probably decelerate in air + return + end + + -- Push item out when stuck inside solid node + local is_stuck = false + local snode = core.get_node_or_nil(pos) + if snode then + local sdef = core.registered_nodes[snode.name] or {} + is_stuck = (sdef.walkable == nil or sdef.walkable == true) + and (sdef.collision_box == nil or sdef.collision_box.type == "regular") + and (sdef.node_box == nil or sdef.node_box.type == "regular") + end + + if is_stuck then + local shootdir + local order = { + {x=1, y=0, z=0}, {x=-1, y=0, z= 0}, + {x=0, y=0, z=1}, {x= 0, y=0, z=-1}, + } + + -- Check which one of the 4 sides is free + for o = 1, #order do + local cnode = core.get_node(vector.add(pos, order[o])).name + local cdef = core.registered_nodes[cnode] or {} + if cnode ~= "ignore" and cdef.walkable == false then + shootdir = order[o] + break + end + end + -- If none of the 4 sides is free, check upwards + if not shootdir then + shootdir = {x=0, y=1, z=0} + local cnode = core.get_node(vector.add(pos, shootdir)).name + if cnode == "ignore" then + shootdir = nil -- Do not push into ignore + end + end + + if shootdir then + -- Set new item moving speed accordingly + local newv = vector.multiply(shootdir, 3) + self:disable_physics() + self.object:set_velocity(newv) + + self.force_out = newv + self.force_out_start = vector.round(pos) + return + end + end + + node = nil -- ground node we're colliding with + if moveresult.touching_ground then + for _, info in ipairs(moveresult.collisions) do + if info.axis == "y" then + node = core.get_node(info.node_pos) + break + end + end + end + + -- Slide on slippery nodes + local def = node and core.registered_nodes[node.name] + local keep_movement = false + + if def then + local slippery = core.get_item_group(node.name, "slippery") + local vel = self.object:get_velocity() + if slippery ~= 0 and (math.abs(vel.x) > 0.1 or math.abs(vel.z) > 0.1) then + -- Horizontal deceleration + local factor = math.min(4 / (slippery + 4) * dtime, 1) + self.object:set_velocity({ + x = vel.x * (1 - factor), + y = 0, + z = vel.z * (1 - factor) + }) + keep_movement = true + end + end + + if not keep_movement then + self.object:set_velocity({x=0, y=0, z=0}) + end + + if self.moving_state == keep_movement then + -- Do not update anything until the moving state changes + return + end + self.moving_state = keep_movement + + -- Only collect items if not moving + if self.moving_state then + return + end + -- Collect the items around to merge with + local own_stack = ItemStack(self.itemstring) + if own_stack:get_free_space() == 0 then + return + end + local objects = core.get_objects_inside_radius(pos, 1.0) + for k, obj in pairs(objects) do + local entity = obj:get_luaentity() + if entity and entity.name == "__builtin:item" then + if self:try_merge_with(own_stack, obj, entity) then + own_stack = ItemStack(self.itemstring) + if own_stack:get_free_space() == 0 then + return + end + end + end + end + end, + + on_punch = function(self, hitter) + local inv = hitter:get_inventory() + if inv and self.itemstring ~= "" then + local left = inv:add_item("main", self.itemstring) + if left and not left:is_empty() then + self:set_item(left) + return + end + end + self.itemstring = "" + self.object:remove() + end, +}) diff --git a/builtin/game/item_s.lua b/builtin/game/item_s.lua new file mode 100644 index 0000000..a51cd0a --- /dev/null +++ b/builtin/game/item_s.lua @@ -0,0 +1,156 @@ +-- Minetest: builtin/item_s.lua +-- The distinction of what goes here is a bit tricky, basically it's everything +-- that does not (directly or indirectly) need access to ServerEnvironment, +-- Server or writable access to IGameDef on the engine side. +-- (The '_s' stands for standalone.) + +-- +-- Item definition helpers +-- + +function core.inventorycube(img1, img2, img3) + img2 = img2 or img1 + img3 = img3 or img1 + return "[inventorycube" + .. "{" .. img1:gsub("%^", "&") + .. "{" .. img2:gsub("%^", "&") + .. "{" .. img3:gsub("%^", "&") +end + +function core.dir_to_facedir(dir, is6d) + --account for y if requested + if is6d and math.abs(dir.y) > math.abs(dir.x) and math.abs(dir.y) > math.abs(dir.z) then + + --from above + if dir.y < 0 then + if math.abs(dir.x) > math.abs(dir.z) then + if dir.x < 0 then + return 19 + else + return 13 + end + else + if dir.z < 0 then + return 10 + else + return 4 + end + end + + --from below + else + if math.abs(dir.x) > math.abs(dir.z) then + if dir.x < 0 then + return 15 + else + return 17 + end + else + if dir.z < 0 then + return 6 + else + return 8 + end + end + end + + --otherwise, place horizontally + elseif math.abs(dir.x) > math.abs(dir.z) then + if dir.x < 0 then + return 3 + else + return 1 + end + else + if dir.z < 0 then + return 2 + else + return 0 + end + end +end + +-- Table of possible dirs +local facedir_to_dir = { + vector.new( 0, 0, 1), + vector.new( 1, 0, 0), + vector.new( 0, 0, -1), + vector.new(-1, 0, 0), + vector.new( 0, -1, 0), + vector.new( 0, 1, 0), +} +-- Mapping from facedir value to index in facedir_to_dir. +local facedir_to_dir_map = { + [0]=1, 2, 3, 4, + 5, 2, 6, 4, + 6, 2, 5, 4, + 1, 5, 3, 6, + 1, 6, 3, 5, + 1, 4, 3, 2, +} +function core.facedir_to_dir(facedir) + return facedir_to_dir[facedir_to_dir_map[facedir % 32]] +end + +function core.dir_to_wallmounted(dir) + if math.abs(dir.y) > math.max(math.abs(dir.x), math.abs(dir.z)) then + if dir.y < 0 then + return 1 + else + return 0 + end + elseif math.abs(dir.x) > math.abs(dir.z) then + if dir.x < 0 then + return 3 + else + return 2 + end + else + if dir.z < 0 then + return 5 + else + return 4 + end + end +end + +-- table of dirs in wallmounted order +local wallmounted_to_dir = { + [0] = vector.new( 0, 1, 0), + vector.new( 0, -1, 0), + vector.new( 1, 0, 0), + vector.new(-1, 0, 0), + vector.new( 0, 0, 1), + vector.new( 0, 0, -1), +} +function core.wallmounted_to_dir(wallmounted) + return wallmounted_to_dir[wallmounted % 8] +end + +function core.dir_to_yaw(dir) + return -math.atan2(dir.x, dir.z) +end + +function core.yaw_to_dir(yaw) + return vector.new(-math.sin(yaw), 0, math.cos(yaw)) +end + +function core.is_colored_paramtype(ptype) + return (ptype == "color") or (ptype == "colorfacedir") or + (ptype == "colorwallmounted") or (ptype == "colordegrotate") +end + +function core.strip_param2_color(param2, paramtype2) + if not core.is_colored_paramtype(paramtype2) then + return nil + end + if paramtype2 == "colorfacedir" then + param2 = math.floor(param2 / 32) * 32 + elseif paramtype2 == "colorwallmounted" then + param2 = math.floor(param2 / 8) * 8 + elseif paramtype2 == "colordegrotate" then + param2 = math.floor(param2 / 32) * 32 + end + -- paramtype2 == "color" requires no modification. + return param2 +end diff --git a/builtin/game/knockback.lua b/builtin/game/knockback.lua new file mode 100644 index 0000000..a937aa1 --- /dev/null +++ b/builtin/game/knockback.lua @@ -0,0 +1,46 @@ +-- can be overriden by mods +function core.calculate_knockback(player, hitter, time_from_last_punch, tool_capabilities, dir, distance, damage) + if damage == 0 or player:get_armor_groups().immortal then + return 0.0 + end + + local m = 8 + -- solve m - m*e^(k*4) = 4 for k + local k = -0.17328 + local res = m - m * math.exp(k * damage) + + if distance < 2.0 then + res = res * 1.1 -- more knockback when closer + elseif distance > 4.0 then + res = res * 0.9 -- less when far away + end + return res +end + +local function vector_absmax(v) + local max, abs = math.max, math.abs + return max(max(abs(v.x), abs(v.y)), abs(v.z)) +end + +core.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool_capabilities, unused_dir, damage) + if player:get_hp() == 0 then + return -- RIP + end + + -- Server::handleCommand_Interact() adds eye offset to one but not the other + -- so the direction is slightly off, calculate it ourselves + local dir = vector.subtract(player:get_pos(), hitter:get_pos()) + local d = vector.length(dir) + if d ~= 0.0 then + dir = vector.divide(dir, d) + end + + local k = core.calculate_knockback(player, hitter, time_from_last_punch, tool_capabilities, dir, d, damage) + + local kdir = vector.multiply(dir, k) + if vector_absmax(kdir) < 1.0 then + return -- barely noticeable, so don't even send + end + + player:add_velocity(kdir) +end) diff --git a/builtin/game/misc.lua b/builtin/game/misc.lua new file mode 100644 index 0000000..997b189 --- /dev/null +++ b/builtin/game/misc.lua @@ -0,0 +1,266 @@ +-- Minetest: builtin/misc.lua + +local S = core.get_translator("__builtin") + +-- +-- Misc. API functions +-- + +-- @spec core.kick_player(String, String) :: Boolean +function core.kick_player(player_name, reason) + if type(reason) == "string" then + reason = "Kicked: " .. reason + else + reason = "Kicked." + end + return core.disconnect_player(player_name, reason) +end + +function core.check_player_privs(name, ...) + if core.is_player(name) then + name = name:get_player_name() + elseif type(name) ~= "string" then + error("core.check_player_privs expects a player or playername as " .. + "argument.", 2) + end + + local requested_privs = {...} + local player_privs = core.get_player_privs(name) + local missing_privileges = {} + + if type(requested_privs[1]) == "table" then + -- We were provided with a table like { privA = true, privB = true }. + for priv, value in pairs(requested_privs[1]) do + if value and not player_privs[priv] then + missing_privileges[#missing_privileges + 1] = priv + end + end + else + -- Only a list, we can process it directly. + for key, priv in pairs(requested_privs) do + if not player_privs[priv] then + missing_privileges[#missing_privileges + 1] = priv + end + end + end + + if #missing_privileges > 0 then + return false, missing_privileges + end + + return true, "" +end + + +function core.send_join_message(player_name) + if not core.is_singleplayer() then + core.chat_send_all("*** " .. S("@1 joined the game.", player_name)) + end +end + + +function core.send_leave_message(player_name, timed_out) + local announcement = "*** " .. S("@1 left the game.", player_name) + if timed_out then + announcement = "*** " .. S("@1 left the game (timed out).", player_name) + end + core.chat_send_all(announcement) +end + + +core.register_on_joinplayer(function(player) + local player_name = player:get_player_name() + if not core.is_singleplayer() then + local status = core.get_server_status(player_name, true) + if status and status ~= "" then + core.chat_send_player(player_name, status) + end + end + core.send_join_message(player_name) +end) + + +core.register_on_leaveplayer(function(player, timed_out) + local player_name = player:get_player_name() + core.send_leave_message(player_name, timed_out) +end) + + +function core.is_player(player) + -- a table being a player is also supported because it quacks sufficiently + -- like a player if it has the is_player function + local t = type(player) + return (t == "userdata" or t == "table") and + type(player.is_player) == "function" and player:is_player() +end + + +function core.player_exists(name) + return core.get_auth_handler().get_auth(name) ~= nil +end + + +-- Returns two position vectors representing a box of `radius` in each +-- direction centered around the player corresponding to `player_name` + +function core.get_player_radius_area(player_name, radius) + local player = core.get_player_by_name(player_name) + if player == nil then + return nil + end + + local p1 = player:get_pos() + local p2 = p1 + + if radius then + p1 = vector.subtract(p1, radius) + p2 = vector.add(p2, radius) + end + + return p1, p2 +end + + +-- To be overriden by protection mods + +function core.is_protected(pos, name) + return false +end + + +function core.record_protection_violation(pos, name) + for _, func in pairs(core.registered_on_protection_violation) do + func(pos, name) + end +end + +-- To be overridden by Creative mods + +local creative_mode_cache = core.settings:get_bool("creative_mode") +function core.is_creative_enabled(name) + return creative_mode_cache +end + +-- Checks if specified volume intersects a protected volume + +function core.is_area_protected(minp, maxp, player_name, interval) + -- 'interval' is the largest allowed interval for the 3D lattice of checks. + + -- Compute the optimal float step 'd' for each axis so that all corners and + -- borders are checked. 'd' will be smaller or equal to 'interval'. + -- Subtracting 1e-4 ensures that the max co-ordinate will be reached by the + -- for loop (which might otherwise not be the case due to rounding errors). + + -- Default to 4 + interval = interval or 4 + local d = {} + + for _, c in pairs({"x", "y", "z"}) do + if minp[c] > maxp[c] then + -- Repair positions: 'minp' > 'maxp' + local tmp = maxp[c] + maxp[c] = minp[c] + minp[c] = tmp + end + + if maxp[c] > minp[c] then + d[c] = (maxp[c] - minp[c]) / + math.ceil((maxp[c] - minp[c]) / interval) - 1e-4 + else + d[c] = 1 -- Any value larger than 0 to avoid division by zero + end + end + + for zf = minp.z, maxp.z, d.z do + local z = math.floor(zf + 0.5) + for yf = minp.y, maxp.y, d.y do + local y = math.floor(yf + 0.5) + for xf = minp.x, maxp.x, d.x do + local x = math.floor(xf + 0.5) + local pos = vector.new(x, y, z) + if core.is_protected(pos, player_name) then + return pos + end + end + end + end + return false +end + + +local raillike_ids = {} +local raillike_cur_id = 0 +function core.raillike_group(name) + local id = raillike_ids[name] + if not id then + raillike_cur_id = raillike_cur_id + 1 + raillike_ids[name] = raillike_cur_id + id = raillike_cur_id + end + return id +end + + +-- HTTP callback interface + +core.set_http_api_lua(function(httpenv) + httpenv.fetch = function(req, callback) + local handle = httpenv.fetch_async(req) + + local function update_http_status() + local res = httpenv.fetch_async_get(handle) + if res.completed then + callback(res) + else + core.after(0, update_http_status) + end + end + core.after(0, update_http_status) + end + + return httpenv +end) +core.set_http_api_lua = nil + + +function core.close_formspec(player_name, formname) + return core.show_formspec(player_name, formname, "") +end + + +function core.cancel_shutdown_requests() + core.request_shutdown("", false, -1) +end + + +-- Used for callback handling with dynamic_add_media +core.dynamic_media_callbacks = {} + + +-- Transfer of certain globals into async environment +-- see builtin/async/game.lua for the other side + +local function copy_filtering(t, seen) + if type(t) == "userdata" or type(t) == "function" then + return true -- don't use nil so presence can still be detected + elseif type(t) ~= "table" then + return t + end + local n = {} + seen = seen or {} + seen[t] = n + for k, v in pairs(t) do + local k_ = seen[k] or copy_filtering(k, seen) + local v_ = seen[v] or copy_filtering(v, seen) + n[k_] = v_ + end + return n +end + +function core.get_globals_to_transfer() + local all = { + registered_items = copy_filtering(core.registered_items), + registered_aliases = core.registered_aliases, + } + return all +end diff --git a/builtin/game/misc_s.lua b/builtin/game/misc_s.lua new file mode 100644 index 0000000..67a0ec6 --- /dev/null +++ b/builtin/game/misc_s.lua @@ -0,0 +1,93 @@ +-- Minetest: builtin/misc_s.lua +-- The distinction of what goes here is a bit tricky, basically it's everything +-- that does not (directly or indirectly) need access to ServerEnvironment, +-- Server or writable access to IGameDef on the engine side. +-- (The '_s' stands for standalone.) + +-- +-- Misc. API functions +-- + +function core.hash_node_position(pos) + return (pos.z + 32768) * 65536 * 65536 + + (pos.y + 32768) * 65536 + + pos.x + 32768 +end + + +function core.get_position_from_hash(hash) + local x = (hash % 65536) - 32768 + hash = math.floor(hash / 65536) + local y = (hash % 65536) - 32768 + hash = math.floor(hash / 65536) + local z = (hash % 65536) - 32768 + return vector.new(x, y, z) +end + + +function core.get_item_group(name, group) + if not core.registered_items[name] or not + core.registered_items[name].groups[group] then + return 0 + end + return core.registered_items[name].groups[group] +end + + +function core.get_node_group(name, group) + core.log("deprecated", "Deprecated usage of get_node_group, use get_item_group instead") + return core.get_item_group(name, group) +end + + +function core.setting_get_pos(name) + local value = core.settings:get(name) + if not value then + return nil + end + return core.string_to_pos(value) +end + + +-- See l_env.cpp for the other functions +function core.get_artificial_light(param1) + return math.floor(param1 / 16) +end + +-- PNG encoder safety wrapper + +local o_encode_png = core.encode_png +function core.encode_png(width, height, data, compression) + if type(width) ~= "number" then + error("Incorrect type for 'width', expected number, got " .. type(width)) + end + if type(height) ~= "number" then + error("Incorrect type for 'height', expected number, got " .. type(height)) + end + + local expected_byte_count = width * height * 4 + + if type(data) ~= "table" and type(data) ~= "string" then + error("Incorrect type for 'data', expected table or string, got " .. type(data)) + end + + local data_length = type(data) == "table" and #data * 4 or string.len(data) + + if data_length ~= expected_byte_count then + error(string.format( + "Incorrect length of 'data', width and height imply %d bytes but %d were provided", + expected_byte_count, + data_length + )) + end + + if type(data) == "table" then + local dataBuf = {} + for i = 1, #data do + dataBuf[i] = core.colorspec_to_bytes(data[i]) + end + data = table.concat(dataBuf) + end + + return o_encode_png(width, height, data, compression or 6) +end diff --git a/builtin/game/privileges.lua b/builtin/game/privileges.lua new file mode 100644 index 0000000..2ff4c09 --- /dev/null +++ b/builtin/game/privileges.lua @@ -0,0 +1,108 @@ +-- Minetest: builtin/privileges.lua + +local S = core.get_translator("__builtin") + +-- +-- Privileges +-- + +core.registered_privileges = {} + +function core.register_privilege(name, param) + local function fill_defaults(def) + if def.give_to_singleplayer == nil then + def.give_to_singleplayer = true + end + if def.give_to_admin == nil then + def.give_to_admin = def.give_to_singleplayer + end + if def.description == nil then + def.description = S("(no description)") + end + end + local def + if type(param) == "table" then + def = param + else + def = {description = param} + end + fill_defaults(def) + core.registered_privileges[name] = def +end + +core.register_privilege("interact", S("Can interact with things and modify the world")) +core.register_privilege("shout", S("Can speak in chat")) + +local basic_privs = + core.string_to_privs((core.settings:get("basic_privs") or "shout,interact")) +local basic_privs_desc = S("Can modify basic privileges (@1)", + core.privs_to_string(basic_privs, ', ')) +core.register_privilege("basic_privs", basic_privs_desc) + +core.register_privilege("privs", S("Can modify privileges")) + +core.register_privilege("teleport", { + description = S("Can teleport self"), + give_to_singleplayer = false, +}) +core.register_privilege("bring", { + description = S("Can teleport other players"), + give_to_singleplayer = false, +}) +core.register_privilege("settime", { + description = S("Can set the time of day using /time"), + give_to_singleplayer = false, +}) +core.register_privilege("server", { + description = S("Can do server maintenance stuff"), + give_to_singleplayer = false, + give_to_admin = true, +}) +core.register_privilege("protection_bypass", { + description = S("Can bypass node protection in the world"), + give_to_singleplayer = false, +}) +core.register_privilege("ban", { + description = S("Can ban and unban players"), + give_to_singleplayer = false, + give_to_admin = true, +}) +core.register_privilege("kick", { + description = S("Can kick players"), + give_to_singleplayer = false, + give_to_admin = true, +}) +core.register_privilege("give", { + description = S("Can use /give and /giveme"), + give_to_singleplayer = false, +}) +core.register_privilege("password", { + description = S("Can use /setpassword and /clearpassword"), + give_to_singleplayer = false, + give_to_admin = true, +}) +core.register_privilege("fly", { + description = S("Can use fly mode"), + give_to_singleplayer = false, +}) +core.register_privilege("fast", { + description = S("Can use fast mode"), + give_to_singleplayer = false, +}) +core.register_privilege("noclip", { + description = S("Can fly through solid nodes using noclip mode"), + give_to_singleplayer = false, +}) +core.register_privilege("rollback", { + description = S("Can use the rollback functionality"), + give_to_singleplayer = false, +}) +core.register_privilege("debug", { + description = S("Can enable wireframe"), + give_to_singleplayer = false, +}) + +core.register_can_bypass_userlimit(function(name, ip) + local privs = core.get_player_privs(name) + return privs["server"] or privs["ban"] or privs["privs"] or privs["password"] +end) diff --git a/builtin/game/register.lua b/builtin/game/register.lua new file mode 100644 index 0000000..8b6f5b9 --- /dev/null +++ b/builtin/game/register.lua @@ -0,0 +1,625 @@ +-- Minetest: builtin/register.lua + +local S = core.get_translator("__builtin") + +-- +-- Make raw registration functions inaccessible to anyone except this file +-- + +local register_item_raw = core.register_item_raw +core.register_item_raw = nil + +local unregister_item_raw = core.unregister_item_raw +core.unregister_item_raw = nil + +local register_alias_raw = core.register_alias_raw +core.register_alias_raw = nil + +-- +-- Item / entity / ABM / LBM registration functions +-- + +core.registered_abms = {} +core.registered_lbms = {} +core.registered_entities = {} +core.registered_items = {} +core.registered_nodes = {} +core.registered_craftitems = {} +core.registered_tools = {} +core.registered_aliases = {} + +-- For tables that are indexed by item name: +-- If table[X] does not exist, default to table[core.registered_aliases[X]] +local alias_metatable = { + __index = function(t, name) + return rawget(t, core.registered_aliases[name]) + end +} +setmetatable(core.registered_items, alias_metatable) +setmetatable(core.registered_nodes, alias_metatable) +setmetatable(core.registered_craftitems, alias_metatable) +setmetatable(core.registered_tools, alias_metatable) + +-- These item names may not be used because they would interfere +-- with legacy itemstrings +local forbidden_item_names = { + MaterialItem = true, + MaterialItem2 = true, + MaterialItem3 = true, + NodeItem = true, + node = true, + CraftItem = true, + craft = true, + MBOItem = true, + ToolItem = true, + tool = true, +} + +local function check_modname_prefix(name) + if name:sub(1,1) == ":" then + -- If the name starts with a colon, we can skip the modname prefix + -- mechanism. + return name:sub(2) + else + -- Enforce that the name starts with the correct mod name. + local expected_prefix = core.get_current_modname() .. ":" + if name:sub(1, #expected_prefix) ~= expected_prefix then + error("Name " .. name .. " does not follow naming conventions: " .. + "\"" .. expected_prefix .. "\" or \":\" prefix required") + end + + -- Enforce that the name only contains letters, numbers and underscores. + local subname = name:sub(#expected_prefix+1) + if subname:find("[^%w_]") then + error("Name " .. name .. " does not follow naming conventions: " .. + "contains unallowed characters") + end + + return name + end +end + +function core.register_abm(spec) + -- Add to core.registered_abms + assert(type(spec.action) == "function", "Required field 'action' of type function") + core.registered_abms[#core.registered_abms + 1] = spec + spec.mod_origin = core.get_current_modname() or "??" +end + +function core.register_lbm(spec) + -- Add to core.registered_lbms + check_modname_prefix(spec.name) + assert(type(spec.action) == "function", "Required field 'action' of type function") + core.registered_lbms[#core.registered_lbms + 1] = spec + spec.mod_origin = core.get_current_modname() or "??" +end + +function core.register_entity(name, prototype) + -- Check name + if name == nil then + error("Unable to register entity: Name is nil") + end + name = check_modname_prefix(tostring(name)) + + prototype.name = name + prototype.__index = prototype -- so that it can be used as a metatable + + -- Add to core.registered_entities + core.registered_entities[name] = prototype + prototype.mod_origin = core.get_current_modname() or "??" +end + +function core.register_item(name, itemdef) + -- Check name + if name == nil then + error("Unable to register item: Name is nil") + end + name = check_modname_prefix(tostring(name)) + if forbidden_item_names[name] then + error("Unable to register item: Name is forbidden: " .. name) + end + itemdef.name = name + + -- Apply defaults and add to registered_* table + if itemdef.type == "node" then + -- Use the nodebox as selection box if it's not set manually + if itemdef.drawtype == "nodebox" and not itemdef.selection_box then + itemdef.selection_box = itemdef.node_box + elseif itemdef.drawtype == "fencelike" and not itemdef.selection_box then + itemdef.selection_box = { + type = "fixed", + fixed = {-1/8, -1/2, -1/8, 1/8, 1/2, 1/8}, + } + end + if itemdef.light_source and itemdef.light_source > core.LIGHT_MAX then + itemdef.light_source = core.LIGHT_MAX + core.log("warning", "Node 'light_source' value exceeds maximum," .. + " limiting to maximum: " ..name) + end + setmetatable(itemdef, {__index = core.nodedef_default}) + core.registered_nodes[itemdef.name] = itemdef + elseif itemdef.type == "craft" then + setmetatable(itemdef, {__index = core.craftitemdef_default}) + core.registered_craftitems[itemdef.name] = itemdef + elseif itemdef.type == "tool" then + setmetatable(itemdef, {__index = core.tooldef_default}) + core.registered_tools[itemdef.name] = itemdef + elseif itemdef.type == "none" then + setmetatable(itemdef, {__index = core.noneitemdef_default}) + else + error("Unable to register item: Type is invalid: " .. dump(itemdef)) + end + + -- Flowing liquid uses param2 + if itemdef.type == "node" and itemdef.liquidtype == "flowing" then + itemdef.paramtype2 = "flowingliquid" + end + + -- BEGIN Legacy stuff + if itemdef.cookresult_itemstring ~= nil and itemdef.cookresult_itemstring ~= "" then + core.register_craft({ + type="cooking", + output=itemdef.cookresult_itemstring, + recipe=itemdef.name, + cooktime=itemdef.furnace_cooktime + }) + end + if itemdef.furnace_burntime ~= nil and itemdef.furnace_burntime >= 0 then + core.register_craft({ + type="fuel", + recipe=itemdef.name, + burntime=itemdef.furnace_burntime + }) + end + -- END Legacy stuff + + itemdef.mod_origin = core.get_current_modname() or "??" + + -- Disable all further modifications + getmetatable(itemdef).__newindex = {} + + --core.log("Registering item: " .. itemdef.name) + core.registered_items[itemdef.name] = itemdef + core.registered_aliases[itemdef.name] = nil + register_item_raw(itemdef) +end + +function core.unregister_item(name) + if not core.registered_items[name] then + core.log("warning", "Not unregistering item " ..name.. + " because it doesn't exist.") + return + end + -- Erase from registered_* table + local type = core.registered_items[name].type + if type == "node" then + core.registered_nodes[name] = nil + elseif type == "craft" then + core.registered_craftitems[name] = nil + elseif type == "tool" then + core.registered_tools[name] = nil + end + core.registered_items[name] = nil + + + unregister_item_raw(name) +end + +function core.register_node(name, nodedef) + nodedef.type = "node" + core.register_item(name, nodedef) +end + +function core.register_craftitem(name, craftitemdef) + craftitemdef.type = "craft" + + -- BEGIN Legacy stuff + if craftitemdef.inventory_image == nil and craftitemdef.image ~= nil then + craftitemdef.inventory_image = craftitemdef.image + end + -- END Legacy stuff + + core.register_item(name, craftitemdef) +end + +function core.register_tool(name, tooldef) + tooldef.type = "tool" + tooldef.stack_max = 1 + + -- BEGIN Legacy stuff + if tooldef.inventory_image == nil and tooldef.image ~= nil then + tooldef.inventory_image = tooldef.image + end + if tooldef.tool_capabilities == nil and + (tooldef.full_punch_interval ~= nil or + tooldef.basetime ~= nil or + tooldef.dt_weight ~= nil or + tooldef.dt_crackiness ~= nil or + tooldef.dt_crumbliness ~= nil or + tooldef.dt_cuttability ~= nil or + tooldef.basedurability ~= nil or + tooldef.dd_weight ~= nil or + tooldef.dd_crackiness ~= nil or + tooldef.dd_crumbliness ~= nil or + tooldef.dd_cuttability ~= nil) then + tooldef.tool_capabilities = { + full_punch_interval = tooldef.full_punch_interval, + basetime = tooldef.basetime, + dt_weight = tooldef.dt_weight, + dt_crackiness = tooldef.dt_crackiness, + dt_crumbliness = tooldef.dt_crumbliness, + dt_cuttability = tooldef.dt_cuttability, + basedurability = tooldef.basedurability, + dd_weight = tooldef.dd_weight, + dd_crackiness = tooldef.dd_crackiness, + dd_crumbliness = tooldef.dd_crumbliness, + dd_cuttability = tooldef.dd_cuttability, + } + end + -- END Legacy stuff + + -- This isn't just legacy, but more of a convenience feature + local toolcaps = tooldef.tool_capabilities + if toolcaps and toolcaps.punch_attack_uses == nil then + for _, cap in pairs(toolcaps.groupcaps or {}) do + local level = (cap.maxlevel or 0) - 1 + if (cap.uses or 0) ~= 0 and level >= 0 then + toolcaps.punch_attack_uses = cap.uses * (3 ^ level) + break + end + end + end + + core.register_item(name, tooldef) +end + +function core.register_alias(name, convert_to) + if forbidden_item_names[name] then + error("Unable to register alias: Name is forbidden: " .. name) + end + if core.registered_items[name] ~= nil then + core.log("warning", "Not registering alias, item with same name" .. + " is already defined: " .. name .. " -> " .. convert_to) + else + --core.log("Registering alias: " .. name .. " -> " .. convert_to) + core.registered_aliases[name] = convert_to + register_alias_raw(name, convert_to) + end +end + +function core.register_alias_force(name, convert_to) + if forbidden_item_names[name] then + error("Unable to register alias: Name is forbidden: " .. name) + end + if core.registered_items[name] ~= nil then + core.unregister_item(name) + core.log("info", "Removed item " ..name.. + " while attempting to force add an alias") + end + --core.log("Registering alias: " .. name .. " -> " .. convert_to) + core.registered_aliases[name] = convert_to + register_alias_raw(name, convert_to) +end + +function core.on_craft(itemstack, player, old_craft_list, craft_inv) + for _, func in ipairs(core.registered_on_crafts) do + -- cast to ItemStack since func() could return a string + itemstack = ItemStack(func(itemstack, player, old_craft_list, craft_inv) or itemstack) + end + return itemstack +end + +function core.craft_predict(itemstack, player, old_craft_list, craft_inv) + for _, func in ipairs(core.registered_craft_predicts) do + -- cast to ItemStack since func() could return a string + itemstack = ItemStack(func(itemstack, player, old_craft_list, craft_inv) or itemstack) + end + return itemstack +end + +-- Alias the forbidden item names to "" so they can't be +-- created via itemstrings (e.g. /give) +for name in pairs(forbidden_item_names) do + core.registered_aliases[name] = "" + register_alias_raw(name, "") +end + +-- +-- Built-in node definitions. Also defined in C. +-- + +core.register_item(":unknown", { + type = "none", + description = S("Unknown Item"), + inventory_image = "unknown_item.png", + on_place = core.item_place, + on_secondary_use = core.item_secondary_use, + on_drop = core.item_drop, + groups = {not_in_creative_inventory=1}, + diggable = true, +}) + +core.register_node(":air", { + description = S("Air"), + inventory_image = "air.png", + wield_image = "air.png", + drawtype = "airlike", + paramtype = "light", + sunlight_propagates = true, + walkable = false, + pointable = false, + diggable = false, + buildable_to = true, + floodable = true, + air_equivalent = true, + drop = "", + groups = {not_in_creative_inventory=1}, +}) + +core.register_node(":ignore", { + description = S("Ignore"), + inventory_image = "ignore.png", + wield_image = "ignore.png", + drawtype = "airlike", + paramtype = "none", + sunlight_propagates = false, + walkable = false, + pointable = false, + diggable = false, + buildable_to = true, -- A way to remove accidentally placed ignores + air_equivalent = true, + drop = "", + groups = {not_in_creative_inventory=1}, + node_placement_prediction = "", + on_place = function(itemstack, placer, pointed_thing) + core.chat_send_player( + placer:get_player_name(), + core.colorize("#FF0000", + S("You can't place 'ignore' nodes!"))) + return "" + end, +}) + +-- The hand (bare definition) +core.register_item(":", { + type = "none", + wield_image = "wieldhand.png", + groups = {not_in_creative_inventory=1}, +}) + + +function core.override_item(name, redefinition) + if redefinition.name ~= nil then + error("Attempt to redefine name of "..name.." to "..dump(redefinition.name), 2) + end + if redefinition.type ~= nil then + error("Attempt to redefine type of "..name.." to "..dump(redefinition.type), 2) + end + local item = core.registered_items[name] + if not item then + error("Attempt to override non-existent item "..name, 2) + end + for k, v in pairs(redefinition) do + rawset(item, k, v) + end + register_item_raw(item) +end + +do + local default = {mod = "??", name = "??"} + core.callback_origins = setmetatable({}, { + __index = function() + return default + end + }) +end + +function core.run_callbacks(callbacks, mode, ...) + assert(type(callbacks) == "table") + local cb_len = #callbacks + if cb_len == 0 then + if mode == 2 or mode == 3 then + return true + elseif mode == 4 or mode == 5 then + return false + end + end + local ret = nil + for i = 1, cb_len do + local origin = core.callback_origins[callbacks[i]] + core.set_last_run_mod(origin.mod) + local cb_ret = callbacks[i](...) + + if mode == 0 and i == 1 then + ret = cb_ret + elseif mode == 1 and i == cb_len then + ret = cb_ret + elseif mode == 2 then + if not cb_ret or i == 1 then + ret = cb_ret + end + elseif mode == 3 then + if cb_ret then + return cb_ret + end + ret = cb_ret + elseif mode == 4 then + if (cb_ret and not ret) or i == 1 then + ret = cb_ret + end + elseif mode == 5 and cb_ret then + return cb_ret + end + end + return ret +end + +function core.run_priv_callbacks(name, priv, caller, method) + local def = core.registered_privileges[priv] + if not def or not def["on_" .. method] or + not def["on_" .. method](name, caller) then + for _, func in ipairs(core["registered_on_priv_" .. method]) do + if not func(name, caller, priv) then + break + end + end + end +end + +-- +-- Callback registration +-- + +local function make_registration() + local t = {} + local registerfunc = function(func) + t[#t + 1] = func + core.callback_origins[func] = { + mod = core.get_current_modname() or "??", + name = debug.getinfo(1, "n").name or "??" + } + --local origin = core.callback_origins[func] + --print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func)) + end + return t, registerfunc +end + +local function make_registration_reverse() + local t = {} + local registerfunc = function(func) + table.insert(t, 1, func) + core.callback_origins[func] = { + mod = core.get_current_modname() or "??", + name = debug.getinfo(1, "n").name or "??" + } + --local origin = core.callback_origins[func] + --print(origin.name .. ": " .. origin.mod .. " registering cbk " .. tostring(func)) + end + return t, registerfunc +end + +local function make_registration_wrap(reg_fn_name, clear_fn_name) + local list = {} + + local orig_reg_fn = core[reg_fn_name] + core[reg_fn_name] = function(def) + local retval = orig_reg_fn(def) + if retval ~= nil then + if def.name ~= nil then + list[def.name] = def + else + list[retval] = def + end + end + return retval + end + + local orig_clear_fn = core[clear_fn_name] + core[clear_fn_name] = function() + for k in pairs(list) do + list[k] = nil + end + return orig_clear_fn() + end + + return list +end + +local function make_wrap_deregistration(reg_fn, clear_fn, list) + local unregister = function (key) + if type(key) ~= "string" then + error("key is not a string", 2) + end + if not list[key] then + error("Attempt to unregister non-existent element - '" .. key .. "'", 2) + end + local temporary_list = table.copy(list) + clear_fn() + for k,v in pairs(temporary_list) do + if key ~= k then + reg_fn(v) + end + end + end + return unregister +end + +core.registered_on_player_hpchanges = { modifiers = { }, loggers = { } } + +function core.registered_on_player_hpchange(player, hp_change, reason) + local last + for i = #core.registered_on_player_hpchanges.modifiers, 1, -1 do + local func = core.registered_on_player_hpchanges.modifiers[i] + hp_change, last = func(player, hp_change, reason) + if type(hp_change) ~= "number" then + local debuginfo = debug.getinfo(func) + error("The register_on_hp_changes function has to return a number at " .. + debuginfo.short_src .. " line " .. debuginfo.linedefined) + end + if last then + break + end + end + for i, func in ipairs(core.registered_on_player_hpchanges.loggers) do + func(player, hp_change, reason) + end + return hp_change +end + +function core.register_on_player_hpchange(func, modifier) + if modifier then + core.registered_on_player_hpchanges.modifiers[#core.registered_on_player_hpchanges.modifiers + 1] = func + else + core.registered_on_player_hpchanges.loggers[#core.registered_on_player_hpchanges.loggers + 1] = func + end + core.callback_origins[func] = { + mod = core.get_current_modname() or "??", + name = debug.getinfo(1, "n").name or "??" + } +end + +core.registered_biomes = make_registration_wrap("register_biome", "clear_registered_biomes") +core.registered_ores = make_registration_wrap("register_ore", "clear_registered_ores") +core.registered_decorations = make_registration_wrap("register_decoration", "clear_registered_decorations") + +core.unregister_biome = make_wrap_deregistration(core.register_biome, + core.clear_registered_biomes, core.registered_biomes) + +core.registered_on_chat_messages, core.register_on_chat_message = make_registration() +core.registered_on_chatcommands, core.register_on_chatcommand = make_registration() +core.registered_globalsteps, core.register_globalstep = make_registration() +core.registered_playerevents, core.register_playerevent = make_registration() +core.registered_on_mods_loaded, core.register_on_mods_loaded = make_registration() +core.registered_on_shutdown, core.register_on_shutdown = make_registration() +core.registered_on_punchnodes, core.register_on_punchnode = make_registration() +core.registered_on_placenodes, core.register_on_placenode = make_registration() +core.registered_on_dignodes, core.register_on_dignode = make_registration() +core.registered_on_generateds, core.register_on_generated = make_registration() +core.registered_on_newplayers, core.register_on_newplayer = make_registration() +core.registered_on_dieplayers, core.register_on_dieplayer = make_registration() +core.registered_on_respawnplayers, core.register_on_respawnplayer = make_registration() +core.registered_on_prejoinplayers, core.register_on_prejoinplayer = make_registration() +core.registered_on_joinplayers, core.register_on_joinplayer = make_registration() +core.registered_on_leaveplayers, core.register_on_leaveplayer = make_registration() +core.registered_on_player_receive_fields, core.register_on_player_receive_fields = make_registration_reverse() +core.registered_on_cheats, core.register_on_cheat = make_registration() +core.registered_on_crafts, core.register_on_craft = make_registration() +core.registered_craft_predicts, core.register_craft_predict = make_registration() +core.registered_on_protection_violation, core.register_on_protection_violation = make_registration() +core.registered_on_item_eats, core.register_on_item_eat = make_registration() +core.registered_on_punchplayers, core.register_on_punchplayer = make_registration() +core.registered_on_priv_grant, core.register_on_priv_grant = make_registration() +core.registered_on_priv_revoke, core.register_on_priv_revoke = make_registration() +core.registered_on_authplayers, core.register_on_authplayer = make_registration() +core.registered_can_bypass_userlimit, core.register_can_bypass_userlimit = make_registration() +core.registered_on_modchannel_message, core.register_on_modchannel_message = make_registration() +core.registered_on_player_inventory_actions, core.register_on_player_inventory_action = make_registration() +core.registered_allow_player_inventory_actions, core.register_allow_player_inventory_action = make_registration() +core.registered_on_rightclickplayers, core.register_on_rightclickplayer = make_registration() +core.registered_on_liquid_transformed, core.register_on_liquid_transformed = make_registration() + +-- +-- Compatibility for on_mapgen_init() +-- + +core.register_on_mapgen_init = function(func) func(core.get_mapgen_params()) end diff --git a/builtin/game/statbars.lua b/builtin/game/statbars.lua new file mode 100644 index 0000000..78d1d27 --- /dev/null +++ b/builtin/game/statbars.lua @@ -0,0 +1,187 @@ +-- cache setting +local enable_damage = core.settings:get_bool("enable_damage") + +local bar_definitions = { + hp = { + hud_elem_type = "statbar", + position = {x = 0.5, y = 1}, + text = "heart.png", + text2 = "heart_gone.png", + number = core.PLAYER_MAX_HP_DEFAULT, + item = core.PLAYER_MAX_HP_DEFAULT, + direction = 0, + size = {x = 24, y = 24}, + offset = {x = (-10 * 24) - 25, y = -(48 + 24 + 16)}, + }, + breath = { + hud_elem_type = "statbar", + position = {x = 0.5, y = 1}, + text = "bubble.png", + text2 = "bubble_gone.png", + number = core.PLAYER_MAX_BREATH_DEFAULT * 2, + item = core.PLAYER_MAX_BREATH_DEFAULT * 2, + direction = 0, + size = {x = 24, y = 24}, + offset = {x = 25, y= -(48 + 24 + 16)}, + }, +} + +local hud_ids = {} + +local function scaleToHudMax(player, field) + -- Scale "hp" or "breath" to the hud maximum dimensions + local current = player["get_" .. field](player) + local nominal = bar_definitions[field].item + local max_display = math.max(player:get_properties()[field .. "_max"], current) + return math.ceil(current / max_display * nominal) +end + +local function update_builtin_statbars(player) + local name = player:get_player_name() + + if name == "" then + return + end + + local flags = player:hud_get_flags() + if not hud_ids[name] then + hud_ids[name] = {} + -- flags are not transmitted to client on connect, we need to make sure + -- our current flags are transmitted by sending them actively + player:hud_set_flags(flags) + end + local hud = hud_ids[name] + + local immortal = player:get_armor_groups().immortal == 1 + + if flags.healthbar and enable_damage and not immortal then + local number = scaleToHudMax(player, "hp") + if hud.id_healthbar == nil then + local hud_def = table.copy(bar_definitions.hp) + hud_def.number = number + hud.id_healthbar = player:hud_add(hud_def) + else + player:hud_change(hud.id_healthbar, "number", number) + end + elseif hud.id_healthbar then + player:hud_remove(hud.id_healthbar) + hud.id_healthbar = nil + end + + local show_breathbar = flags.breathbar and enable_damage and not immortal + + local breath = player:get_breath() + local breath_max = player:get_properties().breath_max + if show_breathbar and breath <= breath_max then + local number = scaleToHudMax(player, "breath") + if not hud.id_breathbar and breath < breath_max then + local hud_def = table.copy(bar_definitions.breath) + hud_def.number = number + hud.id_breathbar = player:hud_add(hud_def) + elseif hud.id_breathbar then + player:hud_change(hud.id_breathbar, "number", number) + end + end + + if hud.id_breathbar and (not show_breathbar or breath == breath_max) then + core.after(1, function(player_name, breath_bar) + local player = core.get_player_by_name(player_name) + if player then + player:hud_remove(breath_bar) + end + end, name, hud.id_breathbar) + hud.id_breathbar = nil + end +end + +local function cleanup_builtin_statbars(player) + local name = player:get_player_name() + + if name == "" then + return + end + + hud_ids[name] = nil +end + +local function player_event_handler(player,eventname) + assert(player:is_player()) + + local name = player:get_player_name() + + if name == "" or not hud_ids[name] then + return + end + + if eventname == "health_changed" then + update_builtin_statbars(player) + + if hud_ids[name].id_healthbar then + return true + end + end + + if eventname == "breath_changed" then + update_builtin_statbars(player) + + if hud_ids[name].id_breathbar then + return true + end + end + + if eventname == "hud_changed" or eventname == "properties_changed" then + update_builtin_statbars(player) + return true + end + + return false +end + +function core.hud_replace_builtin(hud_name, definition) + if type(definition) ~= "table" or + definition.hud_elem_type ~= "statbar" then + return false + end + + definition = table.copy(definition) + + if hud_name == "health" then + definition.item = definition.item or definition.number or core.PLAYER_MAX_HP_DEFAULT + bar_definitions.hp = definition + + for name, ids in pairs(hud_ids) do + local player = core.get_player_by_name(name) + if player and ids.id_healthbar then + player:hud_remove(ids.id_healthbar) + ids.id_healthbar = nil + update_builtin_statbars(player) + end + end + return true + end + + if hud_name == "breath" then + definition.item = definition.item or definition.number or core.PLAYER_MAX_BREATH_DEFAULT + bar_definitions.breath = definition + + for name, ids in pairs(hud_ids) do + local player = core.get_player_by_name(name) + if player and ids.id_breathbar then + player:hud_remove(ids.id_breathbar) + ids.id_breathbar = nil + update_builtin_statbars(player) + end + end + return true + end + + return false +end + +-- Append "update_builtin_statbars" as late as possible +-- This ensures that the HUD is hidden when the flags are updated in this callback +core.register_on_mods_loaded(function() + core.register_on_joinplayer(update_builtin_statbars) +end) +core.register_on_leaveplayer(cleanup_builtin_statbars) +core.register_playerevent(player_event_handler) diff --git a/builtin/game/static_spawn.lua b/builtin/game/static_spawn.lua new file mode 100644 index 0000000..fae23ea --- /dev/null +++ b/builtin/game/static_spawn.lua @@ -0,0 +1,23 @@ +-- Minetest: builtin/static_spawn.lua + +local static_spawnpoint_string = core.settings:get("static_spawnpoint") +if static_spawnpoint_string and + static_spawnpoint_string ~= "" and + not core.setting_get_pos("static_spawnpoint") then + error('The static_spawnpoint setting is invalid: "' .. + static_spawnpoint_string .. '"') +end + +local function put_player_in_spawn(player_obj) + local static_spawnpoint = core.setting_get_pos("static_spawnpoint") + if not static_spawnpoint then + return false + end + core.log("action", "Moving " .. player_obj:get_player_name() .. + " to static spawnpoint at " .. core.pos_to_string(static_spawnpoint)) + player_obj:set_pos(static_spawnpoint) + return true +end + +core.register_on_newplayer(put_player_in_spawn) +core.register_on_respawnplayer(put_player_in_spawn) diff --git a/builtin/game/voxelarea.lua b/builtin/game/voxelarea.lua new file mode 100644 index 0000000..62f07d9 --- /dev/null +++ b/builtin/game/voxelarea.lua @@ -0,0 +1,134 @@ +local math_floor = math.floor +local vector_new = vector.new + +VoxelArea = { + MinEdge = vector_new(1, 1, 1), + MaxEdge = vector_new(0, 0, 0), + ystride = 0, + zstride = 0, +} + +function VoxelArea:new(o) + o = o or {} + setmetatable(o, self) + self.__index = self + + local e = o:getExtent() + o.ystride = e.x + o.zstride = e.x * e.y + + return o +end + +function VoxelArea:getExtent() + local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge + return vector_new( + MaxEdge.x - MinEdge.x + 1, + MaxEdge.y - MinEdge.y + 1, + MaxEdge.z - MinEdge.z + 1 + ) +end + +function VoxelArea:getVolume() + local e = self:getExtent() + return e.x * e.y * e.z +end + +function VoxelArea:index(x, y, z) + local MinEdge = self.MinEdge + local i = (z - MinEdge.z) * self.zstride + + (y - MinEdge.y) * self.ystride + + (x - MinEdge.x) + 1 + return math_floor(i) +end + +function VoxelArea:indexp(p) + local MinEdge = self.MinEdge + local i = (p.z - MinEdge.z) * self.zstride + + (p.y - MinEdge.y) * self.ystride + + (p.x - MinEdge.x) + 1 + return math_floor(i) +end + +function VoxelArea:position(i) + local MinEdge = self.MinEdge + + i = i - 1 + + local z = math_floor(i / self.zstride) + MinEdge.z + i = i % self.zstride + + local y = math_floor(i / self.ystride) + MinEdge.y + i = i % self.ystride + + local x = math_floor(i) + MinEdge.x + + return vector_new(x, y, z) +end + +function VoxelArea:contains(x, y, z) + local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge + return (x >= MinEdge.x) and (x <= MaxEdge.x) and + (y >= MinEdge.y) and (y <= MaxEdge.y) and + (z >= MinEdge.z) and (z <= MaxEdge.z) +end + +function VoxelArea:containsp(p) + local MaxEdge, MinEdge = self.MaxEdge, self.MinEdge + return (p.x >= MinEdge.x) and (p.x <= MaxEdge.x) and + (p.y >= MinEdge.y) and (p.y <= MaxEdge.y) and + (p.z >= MinEdge.z) and (p.z <= MaxEdge.z) +end + +function VoxelArea:containsi(i) + return (i >= 1) and (i <= self:getVolume()) +end + +function VoxelArea:iter(minx, miny, minz, maxx, maxy, maxz) + local i = self:index(minx, miny, minz) - 1 + local xrange = maxx - minx + 1 + local nextaction = i + 1 + xrange + + local y = 0 + local yrange = maxy - miny + 1 + local yreqstride = self.ystride - xrange + + local z = 0 + local zrange = maxz - minz + 1 + local multistride = self.zstride - ((yrange - 1) * self.ystride + xrange) + + return function() + -- continue i until it needs to jump + i = i + 1 + if i ~= nextaction then + return i + end + + -- continue y until maxy is exceeded + y = y + 1 + if y ~= yrange then + -- set i to index(minx, miny + y, minz + z) - 1 + i = i + yreqstride + nextaction = i + xrange + return i + end + + -- continue z until maxz is exceeded + z = z + 1 + if z == zrange then + -- cuboid finished, return nil + return + end + + -- set i to index(minx, miny, minz + z) - 1 + i = i + multistride + + y = 0 + nextaction = i + xrange + return i + end +end + +function VoxelArea:iterp(minp, maxp) + return self:iter(minp.x, minp.y, minp.z, maxp.x, maxp.y, maxp.z) +end diff --git a/builtin/init.lua b/builtin/init.lua new file mode 100644 index 0000000..8691360 --- /dev/null +++ b/builtin/init.lua @@ -0,0 +1,67 @@ +-- +-- This file contains built-in stuff in Minetest implemented in Lua. +-- +-- It is always loaded and executed after registration of the C API, +-- before loading and running any mods. +-- + +-- Initialize some very basic things +function core.debug(...) core.log(table.concat({...}, "\t")) end +if core.print then + local core_print = core.print + -- Override native print and use + -- terminal if that's turned on + function print(...) + local n, t = select("#", ...), {...} + for i = 1, n do + t[i] = tostring(t[i]) + end + core_print(table.concat(t, "\t")) + end + core.print = nil -- don't pollute our namespace +end +math.randomseed(os.time()) +minetest = core + +-- Load other files +local scriptdir = core.get_builtin_path() +local gamepath = scriptdir .. "game" .. DIR_DELIM +local clientpath = scriptdir .. "client" .. DIR_DELIM +local commonpath = scriptdir .. "common" .. DIR_DELIM +local asyncpath = scriptdir .. "async" .. DIR_DELIM + +dofile(commonpath .. "vector.lua") +dofile(commonpath .. "strict.lua") +dofile(commonpath .. "serialize.lua") +dofile(commonpath .. "misc_helpers.lua") + +if INIT == "game" then + dofile(gamepath .. "init.lua") + assert(not core.get_http_api) +elseif INIT == "mainmenu" then + local mm_script = core.settings:get("main_menu_script") + local custom_loaded = false + if mm_script and mm_script ~= "" then + local testfile = io.open(mm_script, "r") + if testfile then + testfile:close() + dofile(mm_script) + custom_loaded = true + core.log("info", "Loaded custom main menu script: "..mm_script) + else + core.log("error", "Failed to load custom main menu script: "..mm_script) + core.log("info", "Falling back to default main menu script") + end + end + if not custom_loaded then + dofile(core.get_mainmenu_path() .. DIR_DELIM .. "init.lua") + end +elseif INIT == "async" then + dofile(asyncpath .. "mainmenu.lua") +elseif INIT == "async_game" then + dofile(asyncpath .. "game.lua") +elseif INIT == "client" then + dofile(clientpath .. "init.lua") +else + error(("Unrecognized builtin initialization type %s!"):format(tostring(INIT))) +end diff --git a/builtin/locale/__builtin.de.tr b/builtin/locale/__builtin.de.tr new file mode 100644 index 0000000..4a17f7a --- /dev/null +++ b/builtin/locale/__builtin.de.tr @@ -0,0 +1,246 @@ +# textdomain: __builtin +Empty command.=Leerer Befehl. +Invalid command: @1=Ungültiger Befehl: @1 +Invalid command usage.=Ungültige Befehlsverwendung. + (@1 s)= (@1 s) +Command execution took @1 s=Befehlsausführung brauchte @1 s +You don't have permission to run this command (missing privileges: @1).=Sie haben keine Erlaubnis, diesen Befehl auszuführen (fehlende Privilegien: @1). +Unable to get position of player @1.=Konnte Position vom Spieler @1 nicht ermitteln. +Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)=Ungültiges Gebietsformat. Erwartet: (x1,y1,z1) (x2,y2,z2) += +Show chat action (e.g., '/me orders a pizza' displays ' orders a pizza')=Chataktion zeigen (z.B. wird „/me isst Pizza“ zu „ isst Pizza“) +Show the name of the server owner=Den Namen des Servereigentümers zeigen +The administrator of this server is @1.=Der Administrator dieses Servers ist @1. +There's no administrator named in the config file.=In der Konfigurationsdatei wurde kein Administrator angegeben. +@1 does not have any privileges.=@1 hat keine Privilegien. +Privileges of @1: @2=Privilegien von @1: @2 +[]=[] +Show privileges of yourself or another player=Ihre eigenen Privilegien oder die eines anderen Spielers anzeigen +Player @1 does not exist.=Spieler @1 existiert nicht. += +Return list of all online players with privilege=Liste aller Spieler mit einem Privileg ausgeben +Invalid parameters (see /help haspriv).=Ungültige Parameter (siehe „/help haspriv“). +Unknown privilege!=Unbekanntes Privileg! +No online player has the "@1" privilege.=Kein online spielender Spieler hat das „@1“-Privileg. +Players online with the "@1" privilege: @2=Derzeit online spielende Spieler mit dem „@1“-Privileg: @2 +Your privileges are insufficient.=Ihre Privilegien sind unzureichend. +Your privileges are insufficient. '@1' only allows you to grant: @2=Ihre Privilegien sind unzureichend. Mit „@1“ können Sie nur folgendes gewähren: @2 +Unknown privilege: @1=Unbekanntes Privileg: @1 +@1 granted you privileges: @2=@1 gewährte Ihnen Privilegien: @2 + ( [, [<...>]] | all)= ( [, [<...>]] | all) +Give privileges to player=Privileg an Spieler vergeben +Invalid parameters (see /help grant).=Ungültige Parameter (siehe „/help grant“). + [, [<...>]] | all= [, [<...>]] | all +Grant privileges to yourself=Privilegien an Ihnen selbst vergeben +Invalid parameters (see /help grantme).=Ungültige Parameter (siehe „/help grantme“). +Your privileges are insufficient. '@1' only allows you to revoke: @2=Ihre Privilegien sind unzureichend. Mit „@1“ können Sie nur folgendes entziehen: @2 +Note: Cannot revoke in singleplayer: @1=Anmerkung: Im Einzelspielermodus kann man folgendes nicht entziehen: @1 +Note: Cannot revoke from admin: @1=Anmerkung: Vom Admin kann man folgendes nicht entziehen: @1 +No privileges were revoked.=Es wurden keine Privilegien entzogen. +@1 revoked privileges from you: @2=@1 entfernte Privilegien von Ihnen: @2 +Remove privileges from player=Privilegien von Spieler entfernen +Invalid parameters (see /help revoke).=Ungültige Parameter (siehe „/help revoke“). +Revoke privileges from yourself=Privilegien von Ihnen selbst entfernen +Invalid parameters (see /help revokeme).=Ungültige Parameter (siehe „/help revokeme“). + = +Set player's password=Passwort von Spieler setzen +Name field required.=Namensfeld benötigt. +Your password was cleared by @1.=Ihr Passwort wurde von @1 geleert. +Password of player "@1" cleared.=Passwort von Spieler „@1“ geleert. +Your password was set by @1.=Ihr Passwort wurde von @1 gesetzt. +Password of player "@1" set.=Passwort von Spieler „@1“ gesetzt. += +Set empty password for a player=Leeres Passwort für einen Spieler setzen +Reload authentication data=Authentifizierungsdaten erneut laden +Done.=Fertig. +Failed.=Fehlgeschlagen. +Remove a player's data=Daten eines Spielers löschen +Player "@1" removed.=Spieler „@1“ gelöscht. +No such player "@1" to remove.=Es gibt keinen Spieler „@1“, der gelöscht werden könnte. +Player "@1" is connected, cannot remove.=Spieler „@1“ ist verbunden, er kann nicht gelöscht werden. +Unhandled remove_player return code @1.=Nicht berücksichtigter remove_player-Rückgabewert @1. +Cannot teleport out of map bounds!=Eine Teleportation außerhalb der Kartengrenzen ist nicht möglich! +Cannot get player with name @1.=Spieler mit Namen @1 kann nicht gefunden werden. +Cannot teleport, @1 is attached to an object!=Teleportation nicht möglich, @1 ist an einem Objekt befestigt! +Teleporting @1 to @2.=Teleportation von @1 nach @2 +One does not teleport to oneself.=Man teleportiert sich doch nicht zu sich selbst. +Cannot get teleportee with name @1.=Der zu teleportierende Spieler mit Namen @1 kann nicht gefunden werden. +Cannot get target player with name @1.=Zielspieler mit Namen @1 kann nicht gefunden werden. +Teleporting @1 to @2 at @3.=Teleportation von @1 zu @2 bei @3 +,, | | ,, | =,, | | ,, | +Teleport to position or player=Zu Position oder Spieler teleportieren +You don't have permission to teleport other players (missing privilege: @1).=Sie haben nicht die Erlaubnis, andere Spieler zu teleportieren (fehlendes Privileg: @1). +([-n] ) | =([-n] ) | +Set or read server configuration setting=Serverkonfigurationseinstellung setzen oder lesen +Failed. Cannot modify secure settings. Edit the settings file manually.=Fehlgeschlagen. Sicherheitseinstellungen können nicht modifiziert werden. Bearbeiten Sie die Einstellungsdatei manuell. +Failed. Use '/set -n ' to create a new setting.=Fehlgeschlagen. Benutzen Sie „/set -n “, um eine neue Einstellung zu erstellen. +@1 @= @2=@1 @= @2 += +Invalid parameters (see /help set).=Ungültige Parameter (siehe „/help set“). +Finished emerging @1 blocks in @2ms.=Fertig mit Erzeugung von @1 Blöcken in @2 ms. +emergeblocks update: @1/@2 blocks emerged (@3%)=emergeblocks-Update: @1/@2 Kartenblöcke geladen (@3%) +(here []) | ( )=(here []) | ( ) +Load (or, if nonexistent, generate) map blocks contained in area pos1 to pos2 ( and must be in parentheses)=Lade (oder, wenn nicht existent, generiere) Kartenblöcke im Gebiet zwischen Pos1 und Pos2 ( und müssen in Klammern stehen) +Started emerge of area ranging from @1 to @2.=Start des Ladevorgangs des Gebiets zwischen @1 und @2. +Delete map blocks contained in area pos1 to pos2 ( and must be in parentheses)=Kartenblöcke innerhalb des Gebiets zwischen Pos1 und Pos2 löschen ( und müssen in Klammern stehen) +Successfully cleared area ranging from @1 to @2.=Gebiet zwischen @1 und @2 erfolgreich geleert. +Failed to clear one or more blocks in area.=Fehlgeschlagen: Ein oder mehrere Kartenblöcke im Gebiet konnten nicht geleert werden. +Resets lighting in the area between pos1 and pos2 ( and must be in parentheses)=Setzt das Licht im Gebiet zwischen Pos1 und Pos2 zurück ( und müssen in Klammern stehen) +Successfully reset light in the area ranging from @1 to @2.=Das Licht im Gebiet zwischen @1 und @2 wurde erfolgreich zurückgesetzt. +Failed to load one or more blocks in area.=Fehlgeschlagen: Ein oder mehrere Kartenblöcke im Gebiet konnten nicht geladen werden. +List mods installed on the server=Installierte Mods auf dem Server auflisten +No mods installed.=Es sind keine Mods installiert. +Cannot give an empty item.=Ein leerer Gegenstand kann nicht gegeben werden. +Cannot give an unknown item.=Ein unbekannter Gegenstand kann nicht gegeben werden. +Giving 'ignore' is not allowed.=„ignore“ darf nicht gegeben werden. +@1 is not a known player.=@1 ist kein bekannter Spieler. +@1 partially added to inventory.=@1 teilweise ins Inventar eingefügt. +@1 could not be added to inventory.=@1 konnte nicht ins Inventar eingefügt werden. +@1 added to inventory.=@1 zum Inventar hinzugefügt. +@1 partially added to inventory of @2.=@1 teilweise ins Inventar von @2 eingefügt. +@1 could not be added to inventory of @2.=@1 konnte nicht ins Inventar von @2 eingefügt werden. +@1 added to inventory of @2.=@1 ins Inventar von @2 eingefügt. + [ []]= [ []] +Give item to player=Gegenstand an Spieler geben +Name and ItemString required.=Name und ItemString benötigt. + [ []]= [ []] +Give item to yourself=Gegenstand Ihnen selbst geben +ItemString required.=ItemString benötigt. + [,,]= [,,] +Spawn entity at given (or your) position=Entity an angegebener (oder Ihrer eigenen) Position spawnen +EntityName required.=EntityName benötigt. +Unable to spawn entity, player is nil.=Entity konnte nicht gespawnt werden, Spieler ist nil. +Cannot spawn an unknown entity.=Ein unbekanntes Entity kann nicht gespawnt werden. +Invalid parameters (@1).=Ungültige Parameter (@1). +@1 spawned.=@1 gespawnt. +@1 failed to spawn.=@1 konnte nicht gespawnt werden. +Destroy item in hand=Gegenstand in der Hand zerstören +Unable to pulverize, no player.=Konnte nicht pulverisieren, kein Spieler. +Unable to pulverize, no item in hand.=Konnte nicht pulverisieren, kein Gegenstand in der Hand. +An item was pulverized.=Ein Gegenstand wurde pulverisiert. +[] [] []=[] [] [] +Check who last touched a node or a node near it within the time specified by . Default: range @= 0, seconds @= 86400 @= 24h, limit @= 5. Set to inf for no time limit=Überprüfen, wer als letztes einen Node oder einen Node in der Nähe innerhalb der in angegebenen Zeitspanne angefasst hat. Standard: Reichweite @= 0, Sekunden @= 86400 @= 24h, Limit @= 5. auf „inf“ setzen, um Zeitlimit zu deaktivieren. +Rollback functions are disabled.=Rollback-Funktionen sind deaktiviert. +That limit is too high!=Dieses Limit ist zu hoch! +Checking @1 ...=Überprüfe @1 ... +Nobody has touched the specified location in @1 seconds.=Niemand hat die angegebene Position seit @1 Sekunden angefasst. +@1 @2 @3 -> @4 @5 seconds ago.=@1 @2 @3 -> @4 vor @5 Sekunden. +Punch a node (range@=@1, seconds@=@2, limit@=@3).=Hauen Sie einen Node (Reichweite@=@1, Sekunden@=@2, Limit@=@3). +( []) | (: [])=( []) | (: []) +Revert actions of a player. Default for is 60. Set to inf for no time limit=Aktionen eines Spielers zurückrollen. Standard für ist 60. auf „inf“ setzen, um Zeitlimit zu deaktivieren +Invalid parameters. See /help rollback and /help rollback_check.=Ungültige Parameter. Siehe /help rollback und /help rollback_check. +Reverting actions of player '@1' since @2 seconds.=Die Aktionen des Spielers „@1“ seit @2 Sekunden werden rückgängig gemacht. +Reverting actions of @1 since @2 seconds.=Die Aktionen von @1 seit @2 Sekunden werden rückgängig gemacht. +(log is too long to show)=(Protokoll ist zu lang für die Anzeige) +Reverting actions succeeded.=Die Aktionen wurden erfolgreich rückgängig gemacht. +Reverting actions FAILED.=FEHLGESCHLAGEN: Die Aktionen konnten nicht rückgängig gemacht werden. +Show server status=Serverstatus anzeigen +This command was disabled by a mod or game.=Dieser Befehl wurde von einer Mod oder einem Spiel deaktiviert. +[<0..23>:<0..59> | <0..24000>]=[<0..23>:<0..59> | <0..24000>] +Show or set time of day=Tageszeit anzeigen oder setzen +Current time is @1:@2.=Es ist jetzt @1:@2 Uhr. +You don't have permission to run this command (missing privilege: @1).=Sie haben nicht die Erlaubnis, diesen Befehl auszuführen (fehlendes Privileg: @1). +Invalid time (must be between 0 and 24000).=Ungültige Zeit (muss zwischen 0 und 24000 liegen). +Time of day changed.=Tageszeit geändert. +Invalid hour (must be between 0 and 23 inclusive).=Ungültige Stunde (muss zwischen 0 und 23 inklusive liegen). +Invalid minute (must be between 0 and 59 inclusive).=Ungültige Minute (muss zwischen 0 und 59 inklusive liegen). +Show day count since world creation=Anzahl Tage seit der Erschaffung der Welt anzeigen +Current day is @1.=Aktueller Tag ist @1. +[ | -1] [-r] []=[ | -1] [-r] [] +Shutdown server (-1 cancels a delayed shutdown, -r allows players to reconnect)=Server herunterfahren (-1 bricht einen verzögerten Abschaltvorgang ab, -r erlaubt Spielern, sich wiederzuverbinden) +Server shutting down (operator request).=Server wird heruntergefahren (Betreiberanfrage). +Ban the IP of a player or show the ban list=Die IP eines Spielers verbannen oder die Bannliste anzeigen +The ban list is empty.=Die Bannliste ist leer. +Ban list: @1=Bannliste: @1 +You cannot ban players in singleplayer!=Im Einzelspielermodus können Sie keine Spieler verbannen! +Player is not online.=Spieler ist nicht online. +Failed to ban player.=Konnte Spieler nicht verbannen. +Banned @1.=@1 verbannt. + | = | +Remove IP ban belonging to a player/IP=Einen IP-Bann auf einen Spieler zurücknehmen +Failed to unban player/IP.=Konnte Bann auf Spieler/IP nicht zurücknehmen. +Unbanned @1.=Bann auf @1 zurückgenommen. + []= [] +Kick a player=Spieler hinauswerfen +Failed to kick player @1.=Spieler @1 konnte nicht hinausgeworfen werden. +Kicked @1.=@1 hinausgeworfen. +[full | quick]=[full | quick] +Clear all objects in world=Alle Objekte in der Welt löschen +Invalid usage, see /help clearobjects.=Ungültige Verwendung, siehe /help clearobjects. +Clearing all objects. This may take a long time. You may experience a timeout. (by @1)=Lösche alle Objekte. Dies kann eine lange Zeit dauern. Eine Netzwerkzeitüberschreitung könnte für Sie auftreten. (von @1) +Cleared all objects.=Alle Objekte gelöscht. + = +Send a direct message to a player=Eine Direktnachricht an einen Spieler senden +Invalid usage, see /help msg.=Ungültige Verwendung, siehe /help msg. +The player @1 is not online.=Der Spieler @1 ist nicht online. +DM from @1: @2=DN von @1: @2 +Message sent.=Nachricht gesendet. +Get the last login time of a player or yourself=Den letzten Loginzeitpunkt eines Spielers oder Ihren eigenen anfragen +@1's last login time was @2.=Letzter Loginzeitpunkt von @1 war @2. +@1's last login time is unknown.=Letzter Loginzeitpunkt von @1 ist unbekannt. +Clear the inventory of yourself or another player=Das Inventar von Ihnen oder einem anderen Spieler leeren +You don't have permission to clear another player's inventory (missing privilege: @1).=Sie haben nicht die Erlaubnis, das Inventar eines anderen Spielers zu leeren (fehlendes Privileg: @1). +@1 cleared your inventory.=@1 hat Ihr Inventar geleert. +Cleared @1's inventory.=Inventar von @1 geleert. +Player must be online to clear inventory!=Spieler muss online sein, um das Inventar leeren zu können! +Players can't be killed, damage has been disabled.=Spieler können nicht getötet werden, Schaden ist deaktiviert. +Player @1 is not online.=Spieler @1 ist nicht online. +You are already dead.=Sie sind schon tot. +@1 is already dead.=@1 ist bereits tot. +@1 has been killed.=@1 wurde getötet. +Kill player or yourself=Einen Spieler oder Sie selbst töten +Invalid parameters (see /help @1).=Ungültige Parameter (siehe „/help @1“). +Too many arguments, try using just /help =Zu viele Argumente. Probieren Sie es mit „/help “ +Available commands: @1=Verfügbare Befehle: @1 +Use '/help ' to get more information, or '/help all' to list everything.=„/help “ benutzen, um mehr Informationen zu erhalten, oder „/help all“, um alles aufzulisten. +Available commands:=Verfügbare Befehle: +Command not available: @1=Befehl nicht verfügbar: @1 +[all | privs | ] [-t]=[all | privs | ] [-t] +Get help for commands or list privileges (-t: output in chat)=Hilfe für Befehle erhalten oder Privilegien auflisten (-t: Ausgabe im Chat) +Command=Befehl +Parameters=Parameter +For more information, click on any entry in the list.=Für mehr Informationen klicken Sie auf einen beliebigen Eintrag in der Liste. +Double-click to copy the entry to the chat history.=Doppelklicken, um den Eintrag in die Chathistorie einzufügen. +Command: @1 @2=Befehl: @1 @2 +Available commands: (see also: /help )=Verfügbare Befehle: (siehe auch: /help ) +Close=Schließen +Privilege=Privileg +Description=Beschreibung +Available privileges:=Verfügbare Privilegien: +print [] | dump [] | save [ []] | reset=print [] | dump [] | save [ []] +Handle the profiler and profiling data=Den Profiler und Profilingdaten verwalten +Statistics written to action log.=Statistiken zum Aktionsprotokoll geschrieben. +Statistics were reset.=Statistiken wurden zurückgesetzt. +Usage: @1=Verwendung: @1 +Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).=Format kann entweder „txt“, „csv“, „lua“, „json“ oder „json_pretty“ sein (die Struktur kann sich in Zukunft ändern). +@1 joined the game.=@1 ist dem Spiel beigetreten. +@1 left the game.=@1 hat das Spiel verlassen. +@1 left the game (timed out).=@1 hat das Spiel verlassen (Netzwerkzeitüberschreitung). +(no description)=(keine Beschreibung) +Can interact with things and modify the world=Kann mit Dingen interagieren und die Welt verändern +Can speak in chat=Kann im Chat sprechen +Can modify basic privileges (@1)=Kann grundlegende Privilegien anpassen (@1) +Can modify privileges=Kann Privilegien anpassen +Can teleport self=Kann sich selbst teleportieren +Can teleport other players=Kann andere Spieler teleportieren +Can set the time of day using /time=Kann die Tageszeit mit /time setzen +Can do server maintenance stuff=Kann Serverwartungsdinge machen +Can bypass node protection in the world=Kann den Schutz auf Blöcken in der Welt umgehen +Can ban and unban players=Kann Spieler verbannen und entbannen +Can kick players=Kann Spieler hinauswerfen +Can use /give and /giveme=Kann /give und /giveme benutzen +Can use /setpassword and /clearpassword=Kann /setpassword und /clearpassword benutzen +Can use fly mode=Kann den Flugmodus benutzen +Can use fast mode=Kann den Schnellmodus benutzen +Can fly through solid nodes using noclip mode=Kann durch feste Blöcke mit dem Geistmodus fliegen +Can use the rollback functionality=Kann die Rollback-Funktionalität benutzen +Can enable wireframe=Kann Drahtmodell aktivieren +Unknown Item=Unbekannter Gegenstand +Air=Luft +Ignore=Ignorieren +You can't place 'ignore' nodes!=Sie können keine „ignore“-Blöcke platzieren! +Values below show absolute/relative times spend per server step by the instrumented function.=Die unten angegebenen Werte zeigen absolute/relative Zeitspannen, die je Server-Step von der instrumentierten Funktion in Anspruch genommen wurden. +A total of @1 sample(s) were taken.=Es wurden insgesamt @1 Datenpunkt(e) aufgezeichnet. +The output is limited to '@1'.=Die Ausgabe ist beschränkt auf „@1“. +Saving of profile failed: @1=Speichern des Profils fehlgeschlagen: @1 +Profile saved to @1=Profil abgespeichert nach @1 diff --git a/builtin/locale/__builtin.it.tr b/builtin/locale/__builtin.it.tr new file mode 100644 index 0000000..b04b489 --- /dev/null +++ b/builtin/locale/__builtin.it.tr @@ -0,0 +1,259 @@ +# textdomain: __builtin +Empty command.=Comando vuoto. +Invalid command: @1=Comando non valido: @1 +Invalid command usage.=Utilizzo del comando non valido. + (@1 s)= +Command execution took @1 s= +You don't have permission to run this command (missing privileges: @1).=Non hai il permesso di eseguire questo comando (privilegi mancanti: @1). +Unable to get position of player @1.=Impossibile ottenere la posizione del giocatore @1. +Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)=Formato dell'area non corretto. Richiesto: (x1,y1,z1) (x2,y2,z2) += +Show chat action (e.g., '/me orders a pizza' displays ' orders a pizza')=Mostra un'azione in chat (es. `/me ordina una pizza` mostra ` ordina una pizza`) +Show the name of the server owner=Mostra il nome del proprietario del server +The administrator of this server is @1.=L'amministratore di questo server è @1. +There's no administrator named in the config file.=Non c'è nessun amministratore nel file di configurazione. +@1 does not have any privileges.= +Privileges of @1: @2=Privilegi di @1: @2 +[]=[] +Show privileges of yourself or another player=Mostra i privilegi propri o di un altro giocatore +Player @1 does not exist.=Il giocatore @1 non esiste. += +Return list of all online players with privilege=Ritorna una lista di tutti i giocatori connessi col tale privilegio +Invalid parameters (see /help haspriv).=Parametri non validi (vedi /help haspriv). +Unknown privilege!=Privilegio sconosciuto! +No online player has the "@1" privilege.= +Players online with the "@1" privilege: @2=Giocatori connessi con il privilegio "@1": @2 +Your privileges are insufficient.=I tuoi privilegi sono insufficienti. +Your privileges are insufficient. '@1' only allows you to grant: @2= +Unknown privilege: @1=Privilegio sconosciuto: @1 +@1 granted you privileges: @2=@1 ti ha assegnato i seguenti privilegi: @2 + ( [, [<...>]] | all)= +Give privileges to player=Dà privilegi al giocatore +Invalid parameters (see /help grant).=Parametri non validi (vedi /help grant). + [, [<...>]] | all= +Grant privileges to yourself=Assegna dei privilegi a te stessǝ +Invalid parameters (see /help grantme).=Parametri non validi (vedi /help grantme). +Your privileges are insufficient. '@1' only allows you to revoke: @2= +Note: Cannot revoke in singleplayer: @1= +Note: Cannot revoke from admin: @1= +No privileges were revoked.= +@1 revoked privileges from you: @2=@1 ti ha revocato i seguenti privilegi: @2 +Remove privileges from player=Rimuove privilegi dal giocatore +Invalid parameters (see /help revoke).=Parametri non validi (vedi /help revoke). +Revoke privileges from yourself=Revoca privilegi a te stessǝ +Invalid parameters (see /help revokeme).=Parametri non validi (vedi /help revokeme). + = +Set player's password=Imposta la password del giocatore +Name field required.=Campo "nome" richiesto. +Your password was cleared by @1.=La tua password è stata resettata da @1. +Password of player "@1" cleared.=Password del giocatore "@1" resettata. +Your password was set by @1.=La tua password è stata impostata da @1. +Password of player "@1" set.=Password del giocatore "@1" impostata. += +Set empty password for a player=Imposta una password vuota a un giocatore +Reload authentication data=Ricarica i dati d'autenticazione +Done.=Fatto. +Failed.=Errore. +Remove a player's data=Rimuove i dati di un giocatore +Player "@1" removed.=Giocatore "@1" rimosso. +No such player "@1" to remove.=Non è presente nessun giocatore "@1" da rimuovere. +Player "@1" is connected, cannot remove.=Il giocatore "@1" è connesso, non può essere rimosso. +Unhandled remove_player return code @1.=Codice ritornato da remove_player non gestito (@1). +Cannot teleport out of map bounds!=Non ci si può teletrasportare fuori dai limiti della mappa! +Cannot get player with name @1.=Impossibile trovare il giocatore chiamato @1. +Cannot teleport, @1 is attached to an object!=Impossibile teletrasportare, @1 è attaccato a un oggetto! +Teleporting @1 to @2.=Teletrasportando @1 da @2. +One does not teleport to oneself.=Non ci si può teletrasportare su se stessi. +Cannot get teleportee with name @1.=Impossibile trovare il giocatore chiamato @1 per il teletrasporto +Cannot get target player with name @1.=Impossibile trovare il giocatore chiamato @1 per il teletrasporto +Teleporting @1 to @2 at @3.=Teletrasportando @1 da @2 a @3 +,, | | ,, | =,, | | ,, | +Teleport to position or player=Teletrasporta a una posizione o da un giocatore +You don't have permission to teleport other players (missing privilege: @1).=Non hai il permesso di teletrasportare altri giocatori (privilegio mancante: @1). +([-n] ) | =([-n] ) | +Set or read server configuration setting=Imposta o ottieni le configurazioni del server +Failed. Cannot modify secure settings. Edit the settings file manually.= +Failed. Use '/set -n ' to create a new setting.=Errore. Usa 'set -n ' per creare una nuova impostazione +@1 @= @2=@1 @= @2 += +Invalid parameters (see /help set).=Parametri non validi (vedi /help set). +Finished emerging @1 blocks in @2ms.=Finito di emergere @1 blocchi in @2ms +emergeblocks update: @1/@2 blocks emerged (@3%)=aggiornamento emergeblocks: @1/@2 blocchi emersi (@3%) +(here []) | ( )=(here []) | ( ) +Load (or, if nonexistent, generate) map blocks contained in area pos1 to pos2 ( and must be in parentheses)=Carica (o, se non esiste, genera) blocchi mappa contenuti nell'area tra pos1 e pos2 ( e vanno tra parentesi) +Started emerge of area ranging from @1 to @2.=Iniziata emersione dell'area tra @1 e @2. +Delete map blocks contained in area pos1 to pos2 ( and must be in parentheses)=Cancella i blocchi mappa contenuti nell'area tra pos1 e pos2 ( e vanno tra parentesi) +Successfully cleared area ranging from @1 to @2.=Area tra @1 e @2 ripulita con successo. +Failed to clear one or more blocks in area.=Errore nel ripulire uno o più blocchi mappa nell'area +Resets lighting in the area between pos1 and pos2 ( and must be in parentheses)=Reimposta l'illuminazione nell'area tra pos1 e po2 ( e vanno tra parentesi) +Successfully reset light in the area ranging from @1 to @2.=Luce nell'area tra @1 e @2 reimpostata con successo. +Failed to load one or more blocks in area.=Errore nel caricare uno o più blocchi mappa nell'area. +List mods installed on the server=Elenca le mod installate nel server +No mods installed.= +Cannot give an empty item.=Impossibile dare un oggetto vuoto. +Cannot give an unknown item.=Impossibile dare un oggetto sconosciuto. +Giving 'ignore' is not allowed.=Non è permesso dare 'ignore'. +@1 is not a known player.=@1 non è un giocatore conosciuto. +@1 partially added to inventory.=@1 parzialmente aggiunto all'inventario. +@1 could not be added to inventory.=@1 non può essere aggiunto all'inventario. +@1 added to inventory.=@1 aggiunto all'inventario. +@1 partially added to inventory of @2.=@1 parzialmente aggiunto all'inventario di @2. +@1 could not be added to inventory of @2.=Non è stato possibile aggiungere @1 all'inventario di @2. +@1 added to inventory of @2.=@1 aggiunto all'inventario di @2. + [ []]= [ []] +Give item to player=Dà oggetti ai giocatori +Name and ItemString required.=Richiesti nome e NomeOggetto. + [ []]= [ []] +Give item to yourself=Dà oggetti a te stessǝ +ItemString required.=Richiesto NomeOggetto. + [,,]= [,,] +Spawn entity at given (or your) position=Genera un'entità alla data coordinata (o la tua) +EntityName required.=Richiesto NomeEntità +Unable to spawn entity, player is nil.=Impossibile generare l'entità, il giocatore è nil. +Cannot spawn an unknown entity.=Impossibile generare un'entità sconosciuta. +Invalid parameters (@1).=Parametri non validi (@1). +@1 spawned.=Generata entità @1. +@1 failed to spawn.=Errore nel generare @1 +Destroy item in hand=Distrugge l'oggetto in mano +Unable to pulverize, no player.=Impossibile polverizzare, nessun giocatore. +Unable to pulverize, no item in hand.=Impossibile polverizzare, nessun oggetto in mano. +An item was pulverized.=Un oggetto è stato polverizzato. +[] [] []=[] [] [] +Check who last touched a node or a node near it within the time specified by . Default: range @= 0, seconds @= 86400 @= 24h, limit @= 5. Set to inf for no time limit=Controlla chi è l'ultimo giocatore che ha toccato un nodo o un nodo nelle sue vicinanze, negli ultimi secondi indicati. Di base: raggio @= 0, secondi @= 86400 @= 24h, limite @= 5. +Rollback functions are disabled.=Le funzioni di rollback sono disabilitate. +That limit is too high!=Il limite è troppo alto! +Checking @1 ...=Controllando @1 ... +Nobody has touched the specified location in @1 seconds.=Nessuno ha toccato il punto specificato negli ultimi @1 secondi. +@1 @2 @3 -> @4 @5 seconds ago.=@1 @2 @3 -> @4 @5 secondi fa. +Punch a node (range@=@1, seconds@=@2, limit@=@3).=Colpisce un nodo (raggio@=@1, secondi@=@2, limite@=@3) +( []) | (: [])=( []) | (: []) +Revert actions of a player. Default for is 60. Set to inf for no time limit=Riavvolge le azioni di un giocatore. Di base, è 60. Imposta a inf per nessun limite di tempo +Invalid parameters. See /help rollback and /help rollback_check.=Parametri non validi. Vedi /help rollback e /help rollback_check. +Reverting actions of player '@1' since @2 seconds.=Riavvolge le azioni del giocatore '@1' avvenute negli ultimi @2 secondi. +Reverting actions of @1 since @2 seconds.=Riavvolge le azioni di @1 avvenute negli ultimi @2 secondi. +(log is too long to show)=(il log è troppo lungo per essere mostrato) +Reverting actions succeeded.=Riavvolgimento azioni avvenuto con successo. +Reverting actions FAILED.=Errore nel riavvolgere le azioni. +Show server status=Mostra lo stato del server +This command was disabled by a mod or game.=Questo comando è stato disabilitato da una mod o dal gioco. +[<0..23>:<0..59> | <0..24000>]=[<0..23>:<0..59> | <0..24000>] +Show or set time of day=Mostra o imposta l'orario della giornata +Current time is @1:@2.=Orario corrente: @1:@2. +You don't have permission to run this command (missing privilege: @1).=Non hai il permesso di eseguire questo comando (privilegio mancante: @1) +Invalid time (must be between 0 and 24000).= +Time of day changed.=Orario della giornata cambiato. +Invalid hour (must be between 0 and 23 inclusive).=Ora non valida (deve essere tra 0 e 23 inclusi) +Invalid minute (must be between 0 and 59 inclusive).=Minuto non valido (deve essere tra 0 e 59 inclusi) +Show day count since world creation=Mostra il conteggio dei giorni da quando il mondo è stato creato +Current day is @1.=Giorno attuale: @1. +[ | -1] [-r] []= +Shutdown server (-1 cancels a delayed shutdown, -r allows players to reconnect)= +Server shutting down (operator request).=Arresto del server in corso (per richiesta dell'operatore) +Ban the IP of a player or show the ban list=Bandisce l'IP del giocatore o mostra la lista di quelli banditi +The ban list is empty.=La lista banditi è vuota. +Ban list: @1=Lista banditi: @1 +You cannot ban players in singleplayer!= +Player is not online.=Il giocatore non è connesso. +Failed to ban player.=Errore nel bandire il giocatore. +Banned @1.=@1 banditǝ. + | = | +Remove IP ban belonging to a player/IP=Perdona l'IP appartenente a un giocatore/IP +Failed to unban player/IP.=Errore nel perdonare il giocatore/IP +Unbanned @1.=@1 perdonatǝ + []= [] +Kick a player=Caccia un giocatore +Failed to kick player @1.=Errore nel cacciare il giocatore @1. +Kicked @1.=@1 cacciatǝ. +[full | quick]=[full | quick] +Clear all objects in world=Elimina tutti gli oggetti/entità nel mondo +Invalid usage, see /help clearobjects.=Uso incorretto, vedi /help clearobjects. +Clearing all objects. This may take a long time. You may experience a timeout. (by @1)=Eliminando tutti gli oggetti/entità. Questo potrebbe richiedere molto tempo e farti eventualmente crashare. (di @1) +Cleared all objects.=Tutti gli oggetti sono stati eliminati. + = +Send a direct message to a player=Invia un messaggio privato al giocatore +Invalid usage, see /help msg.=Uso incorretto, vedi /help msg +The player @1 is not online.=Il giocatore @1 non è connesso. +DM from @1: @2=Messaggio privato da @1: @2 +Message sent.=Messaggio inviato. +Get the last login time of a player or yourself=Ritorna l'ultimo accesso di un giocatore o di te stessǝ +@1's last login time was @2.=L'ultimo accesso di @1 è avvenuto il @2 +@1's last login time is unknown.=L'ultimo accesso di @1 non è conosciuto +Clear the inventory of yourself or another player=Svuota l'inventario tuo o di un altro giocatore +You don't have permission to clear another player's inventory (missing privilege: @1).=Non hai il permesso di svuotare l'inventario di un altro giocatore (privilegio mancante: @1). +@1 cleared your inventory.=@1 ha svuotato il tuo inventario. +Cleared @1's inventory.=L'inventario di @1 è stato svuotato. +Player must be online to clear inventory!=Il giocatore deve essere connesso per svuotarne l'inventario! +Players can't be killed, damage has been disabled.=I giocatori non possono essere uccisi, il danno è disabilitato. +Player @1 is not online.=Il giocatore @1 non è connesso. +You are already dead.=Sei già mortǝ. +@1 is already dead.=@1 è già mortǝ. +@1 has been killed.=@1 è stato uccisǝ. +Kill player or yourself=Uccide un giocatore o te stessǝ +Invalid parameters (see /help @1).= +Too many arguments, try using just /help = +Available commands: @1=Comandi disponibili: @1 +Use '/help ' to get more information, or '/help all' to list everything.=Usa '/help ' per ottenere più informazioni, o '/help all' per elencare tutti i comandi. +Available commands:=Comandi disponibili: +Command not available: @1=Comando non disponibile: @1 +[all | privs | ] [-t]= +Get help for commands or list privileges (-t: output in chat)= +Command=Comando +Parameters=Parametri +For more information, click on any entry in the list.=Per più informazioni, clicca su una qualsiasi voce dell'elenco. +Double-click to copy the entry to the chat history.=Doppio click per copiare la voce nella cronologia della chat. +Command: @1 @2=Comando: @1 @2 +Available commands: (see also: /help )=Comandi disponibili: (vedi anche /help ) +Close=Chiudi +Privilege=Privilegio +Description=Descrizione +Available privileges:=Privilegi disponibili: +print [] | dump [] | save [ []] | reset=print [] | dump [] | save [ []] | reset +Handle the profiler and profiling data=Gestisce il profiler e i dati da esso elaborati +Statistics written to action log.=Statistiche scritte nel log delle azioni. +Statistics were reset.=Le statistiche sono state resettate. +Usage: @1=Utilizzo: @1 +Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).=I formati supportati sono txt, csv, lua, json e json_pretty (le strutture potrebbero essere soggetti a cambiamenti). +@1 joined the game.= +@1 left the game.= +@1 left the game (timed out).= +(no description)=(nessuna descrizione) +Can interact with things and modify the world=Si può interagire con le cose e modificare il mondo +Can speak in chat=Si può parlare in chat +Can modify basic privileges (@1)= +Can modify privileges=Si possono modificare i privilegi +Can teleport self=Si può teletrasportare se stessз +Can teleport other players=Si possono teletrasportare gli altri giocatori +Can set the time of day using /time=Si può impostate l'orario della giornata tramite /time +Can do server maintenance stuff=Si possono eseguire operazioni di manutenzione del server +Can bypass node protection in the world=Si può aggirare la protezione dei nodi nel mondo +Can ban and unban players=Si possono bandire e perdonare i giocatori +Can kick players=Si possono cacciare i giocatori +Can use /give and /giveme=Si possono usare /give e /give me +Can use /setpassword and /clearpassword=Si possono usare /setpassword e /clearpassword +Can use fly mode=Si può usare la modalità volo +Can use fast mode=Si può usare la modalità rapida +Can fly through solid nodes using noclip mode=Si può volare attraverso i nodi solidi con la modalità incorporea +Can use the rollback functionality=Si può usare la funzione di rollback +Can enable wireframe= +Unknown Item=Oggetto sconosciuto +Air=Aria +Ignore=Ignora +You can't place 'ignore' nodes!=Non puoi piazzare nodi 'ignore'! +Values below show absolute/relative times spend per server step by the instrumented function.= +A total of @1 sample(s) were taken.= +The output is limited to '@1'.= +Saving of profile failed: @1= +Profile saved to @1= + + +##### not used anymore ##### + +Invalid time.=Orario non valido. +[all | privs | ]=[all | privs | ] +Get help for commands or list privileges=Richiama la finestra d'aiuto dei comandi o dei privilegi +Allows enabling various debug options that may affect gameplay=Permette di abilitare varie opzioni di debug che potrebbero influenzare l'esperienza di gioco +[ | -1] [reconnect] []=[ | -1] [reconnect] [] +Shutdown server (-1 cancels a delayed shutdown)=Arresta il server (-1 annulla un arresto programmato) + ( | all)= ( | all) + | all= | all +Can modify 'shout' and 'interact' privileges=Si possono modificare i privilegi 'shout' e 'interact' diff --git a/builtin/locale/template.txt b/builtin/locale/template.txt new file mode 100644 index 0000000..fa73523 --- /dev/null +++ b/builtin/locale/template.txt @@ -0,0 +1,246 @@ +# textdomain: __builtin +Empty command.= +Invalid command: @1= +Invalid command usage.= + (@1 s)= +Command execution took @1 s= +You don't have permission to run this command (missing privileges: @1).= +Unable to get position of player @1.= +Incorrect area format. Expected: (x1,y1,z1) (x2,y2,z2)= += +Show chat action (e.g., '/me orders a pizza' displays ' orders a pizza')= +Show the name of the server owner= +The administrator of this server is @1.= +There's no administrator named in the config file.= +@1 does not have any privileges.= +Privileges of @1: @2= +[]= +Show privileges of yourself or another player= +Player @1 does not exist.= += +Return list of all online players with privilege= +Invalid parameters (see /help haspriv).= +Unknown privilege!= +No online player has the "@1" privilege.= +Players online with the "@1" privilege: @2= +Your privileges are insufficient.= +Your privileges are insufficient. '@1' only allows you to grant: @2= +Unknown privilege: @1= +@1 granted you privileges: @2= + ( [, [<...>]] | all)= +Give privileges to player= +Invalid parameters (see /help grant).= + [, [<...>]] | all= +Grant privileges to yourself= +Invalid parameters (see /help grantme).= +Your privileges are insufficient. '@1' only allows you to revoke: @2= +Note: Cannot revoke in singleplayer: @1= +Note: Cannot revoke from admin: @1= +No privileges were revoked.= +@1 revoked privileges from you: @2= +Remove privileges from player= +Invalid parameters (see /help revoke).= +Revoke privileges from yourself= +Invalid parameters (see /help revokeme).= + = +Set player's password= +Name field required.= +Your password was cleared by @1.= +Password of player "@1" cleared.= +Your password was set by @1.= +Password of player "@1" set.= += +Set empty password for a player= +Reload authentication data= +Done.= +Failed.= +Remove a player's data= +Player "@1" removed.= +No such player "@1" to remove.= +Player "@1" is connected, cannot remove.= +Unhandled remove_player return code @1.= +Cannot teleport out of map bounds!= +Cannot get player with name @1.= +Cannot teleport, @1 is attached to an object!= +Teleporting @1 to @2.= +One does not teleport to oneself.= +Cannot get teleportee with name @1.= +Cannot get target player with name @1.= +Teleporting @1 to @2 at @3.= +,, | | ,, | = +Teleport to position or player= +You don't have permission to teleport other players (missing privilege: @1).= +([-n] ) | = +Set or read server configuration setting= +Failed. Cannot modify secure settings. Edit the settings file manually.= +Failed. Use '/set -n ' to create a new setting.= +@1 @= @2= += +Invalid parameters (see /help set).= +Finished emerging @1 blocks in @2ms.= +emergeblocks update: @1/@2 blocks emerged (@3%)= +(here []) | ( )= +Load (or, if nonexistent, generate) map blocks contained in area pos1 to pos2 ( and must be in parentheses)= +Started emerge of area ranging from @1 to @2.= +Delete map blocks contained in area pos1 to pos2 ( and must be in parentheses)= +Successfully cleared area ranging from @1 to @2.= +Failed to clear one or more blocks in area.= +Resets lighting in the area between pos1 and pos2 ( and must be in parentheses)= +Successfully reset light in the area ranging from @1 to @2.= +Failed to load one or more blocks in area.= +List mods installed on the server= +No mods installed.= +Cannot give an empty item.= +Cannot give an unknown item.= +Giving 'ignore' is not allowed.= +@1 is not a known player.= +@1 partially added to inventory.= +@1 could not be added to inventory.= +@1 added to inventory.= +@1 partially added to inventory of @2.= +@1 could not be added to inventory of @2.= +@1 added to inventory of @2.= + [ []]= +Give item to player= +Name and ItemString required.= + [ []]= +Give item to yourself= +ItemString required.= + [,,]= +Spawn entity at given (or your) position= +EntityName required.= +Unable to spawn entity, player is nil.= +Cannot spawn an unknown entity.= +Invalid parameters (@1).= +@1 spawned.= +@1 failed to spawn.= +Destroy item in hand= +Unable to pulverize, no player.= +Unable to pulverize, no item in hand.= +An item was pulverized.= +[] [] []= +Check who last touched a node or a node near it within the time specified by . Default: range @= 0, seconds @= 86400 @= 24h, limit @= 5. Set to inf for no time limit= +Rollback functions are disabled.= +That limit is too high!= +Checking @1 ...= +Nobody has touched the specified location in @1 seconds.= +@1 @2 @3 -> @4 @5 seconds ago.= +Punch a node (range@=@1, seconds@=@2, limit@=@3).= +( []) | (: [])= +Revert actions of a player. Default for is 60. Set to inf for no time limit= +Invalid parameters. See /help rollback and /help rollback_check.= +Reverting actions of player '@1' since @2 seconds.= +Reverting actions of @1 since @2 seconds.= +(log is too long to show)= +Reverting actions succeeded.= +Reverting actions FAILED.= +Show server status= +This command was disabled by a mod or game.= +[<0..23>:<0..59> | <0..24000>]= +Show or set time of day= +Current time is @1:@2.= +You don't have permission to run this command (missing privilege: @1).= +Invalid time (must be between 0 and 24000).= +Time of day changed.= +Invalid hour (must be between 0 and 23 inclusive).= +Invalid minute (must be between 0 and 59 inclusive).= +Show day count since world creation= +Current day is @1.= +[ | -1] [-r] []= +Shutdown server (-1 cancels a delayed shutdown, -r allows players to reconnect)= +Server shutting down (operator request).= +Ban the IP of a player or show the ban list= +The ban list is empty.= +Ban list: @1= +You cannot ban players in singleplayer!= +Player is not online.= +Failed to ban player.= +Banned @1.= + | = +Remove IP ban belonging to a player/IP= +Failed to unban player/IP.= +Unbanned @1.= + []= +Kick a player= +Failed to kick player @1.= +Kicked @1.= +[full | quick]= +Clear all objects in world= +Invalid usage, see /help clearobjects.= +Clearing all objects. This may take a long time. You may experience a timeout. (by @1)= +Cleared all objects.= + = +Send a direct message to a player= +Invalid usage, see /help msg.= +The player @1 is not online.= +DM from @1: @2= +Message sent.= +Get the last login time of a player or yourself= +@1's last login time was @2.= +@1's last login time is unknown.= +Clear the inventory of yourself or another player= +You don't have permission to clear another player's inventory (missing privilege: @1).= +@1 cleared your inventory.= +Cleared @1's inventory.= +Player must be online to clear inventory!= +Players can't be killed, damage has been disabled.= +Player @1 is not online.= +You are already dead.= +@1 is already dead.= +@1 has been killed.= +Kill player or yourself= +Invalid parameters (see /help @1).= +Too many arguments, try using just /help = +Available commands: @1= +Use '/help ' to get more information, or '/help all' to list everything.= +Available commands:= +Command not available: @1= +[all | privs | ] [-t]= +Get help for commands or list privileges (-t: output in chat)= +Command= +Parameters= +For more information, click on any entry in the list.= +Double-click to copy the entry to the chat history.= +Command: @1 @2= +Available commands: (see also: /help )= +Close= +Privilege= +Description= +Available privileges:= +print [] | dump [] | save [ []] | reset= +Handle the profiler and profiling data= +Statistics written to action log.= +Statistics were reset.= +Usage: @1= +Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).= +@1 joined the game.= +@1 left the game.= +@1 left the game (timed out).= +(no description)= +Can interact with things and modify the world= +Can speak in chat= +Can modify basic privileges (@1)= +Can modify privileges= +Can teleport self= +Can teleport other players= +Can set the time of day using /time= +Can do server maintenance stuff= +Can bypass node protection in the world= +Can ban and unban players= +Can kick players= +Can use /give and /giveme= +Can use /setpassword and /clearpassword= +Can use fly mode= +Can use fast mode= +Can fly through solid nodes using noclip mode= +Can use the rollback functionality= +Can enable wireframe= +Unknown Item= +Air= +Ignore= +You can't place 'ignore' nodes!= +Values below show absolute/relative times spend per server step by the instrumented function.= +A total of @1 sample(s) were taken.= +The output is limited to '@1'.= +Saving of profile failed: @1= +Profile saved to @1= diff --git a/builtin/mainmenu/async_event.lua b/builtin/mainmenu/async_event.lua new file mode 100644 index 0000000..04bfb78 --- /dev/null +++ b/builtin/mainmenu/async_event.lua @@ -0,0 +1,32 @@ + +core.async_jobs = {} + +local function handle_job(jobid, serialized_retval) + local retval = core.deserialize(serialized_retval) + assert(type(core.async_jobs[jobid]) == "function") + core.async_jobs[jobid](retval) + core.async_jobs[jobid] = nil +end + +core.async_event_handler = handle_job + +function core.handle_async(func, parameter, callback) + -- Serialize function + local serialized_func = string.dump(func) + + assert(serialized_func ~= nil) + + -- Serialize parameters + local serialized_param = core.serialize(parameter) + + if serialized_param == nil then + return false + end + + local jobid = core.do_async_callback(serialized_func, serialized_param) + + core.async_jobs[jobid] = callback + + return true +end + diff --git a/builtin/mainmenu/common.lua b/builtin/mainmenu/common.lua new file mode 100644 index 0000000..81e28f2 --- /dev/null +++ b/builtin/mainmenu/common.lua @@ -0,0 +1,246 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-- Global menu data +menudata = {} + +-- Local cached values +local min_supp_proto, max_supp_proto + +function common_update_cached_supp_proto() + min_supp_proto = core.get_min_supp_proto() + max_supp_proto = core.get_max_supp_proto() +end +common_update_cached_supp_proto() + +-- Menu helper functions + +local function render_client_count(n) + if n > 999 then return '99+' + elseif n >= 0 then return tostring(n) + else return '?' end +end + +local function configure_selected_world_params(idx) + local worldconfig = pkgmgr.get_worldconfig(menudata.worldlist:get_list()[idx].path) + if worldconfig.creative_mode then + core.settings:set("creative_mode", worldconfig.creative_mode) + end + if worldconfig.enable_damage then + core.settings:set("enable_damage", worldconfig.enable_damage) + end +end + +function render_serverlist_row(spec) + local text = "" + if spec.name then + text = text .. core.formspec_escape(spec.name:trim()) + elseif spec.address then + text = text .. core.formspec_escape(spec.address:trim()) + if spec.port then + text = text .. ":" .. spec.port + end + end + + local grey_out = not spec.is_compatible + + local details = {} + + if spec.lag or spec.ping then + local lag = (spec.lag or 0) * 1000 + (spec.ping or 0) * 250 + if lag <= 125 then + table.insert(details, "1") + elseif lag <= 175 then + table.insert(details, "2") + elseif lag <= 250 then + table.insert(details, "3") + else + table.insert(details, "4") + end + else + table.insert(details, "0") + end + + table.insert(details, ",") + + local color = (grey_out and "#aaaaaa") or ((spec.is_favorite and "#ddddaa") or "#ffffff") + if spec.clients and (spec.clients_max or 0) > 0 then + local clients_percent = 100 * spec.clients / spec.clients_max + + -- Choose a color depending on how many clients are connected + -- (relatively to clients_max) + local clients_color + if grey_out then clients_color = '#aaaaaa' + elseif spec.clients == 0 then clients_color = '' -- 0 players: default/white + elseif clients_percent <= 60 then clients_color = '#a1e587' -- 0-60%: green + elseif clients_percent <= 90 then clients_color = '#ffdc97' -- 60-90%: yellow + elseif clients_percent == 100 then clients_color = '#dd5b5b' -- full server: red (darker) + else clients_color = '#ffba97' -- 90-100%: orange + end + + table.insert(details, clients_color) + table.insert(details, render_client_count(spec.clients) .. " / " .. + render_client_count(spec.clients_max)) + else + table.insert(details, color) + table.insert(details, "?") + end + + if spec.creative then + table.insert(details, "1") -- creative icon + else + table.insert(details, "0") + end + + if spec.pvp then + table.insert(details, "2") -- pvp icon + elseif spec.damage then + table.insert(details, "1") -- heart icon + else + table.insert(details, "0") + end + + table.insert(details, color) + table.insert(details, text) + + return table.concat(details, ",") +end +--------------------------------------------------------------------------------- +os.tmpname = function() + error('do not use') -- instead use core.get_temp_path() +end +-------------------------------------------------------------------------------- + +function menu_render_worldlist() + local retval = {} + local current_worldlist = menudata.worldlist:get_list() + + for i, v in ipairs(current_worldlist) do + retval[#retval+1] = core.formspec_escape(v.name) + end + + return table.concat(retval, ",") +end + +function menu_handle_key_up_down(fields, textlist, settingname) + local oldidx, newidx = core.get_textlist_index(textlist), 1 + if fields.key_up or fields.key_down then + if fields.key_up and oldidx and oldidx > 1 then + newidx = oldidx - 1 + elseif fields.key_down and oldidx and + oldidx < menudata.worldlist:size() then + newidx = oldidx + 1 + end + core.settings:set(settingname, menudata.worldlist:get_raw_index(newidx)) + configure_selected_world_params(newidx) + return true + end + return false +end + +function text2textlist(xpos, ypos, width, height, tl_name, textlen, text, transparency) + local textlines = core.wrap_text(text, textlen, true) + local retval = "textlist[" .. xpos .. "," .. ypos .. ";" .. width .. + "," .. height .. ";" .. tl_name .. ";" + + for i = 1, #textlines do + textlines[i] = textlines[i]:gsub("\r", "") + retval = retval .. core.formspec_escape(textlines[i]) .. "," + end + + retval = retval .. ";0;" + if transparency then retval = retval .. "true" end + retval = retval .. "]" + + return retval +end + +function is_server_protocol_compat(server_proto_min, server_proto_max) + if (not server_proto_min) or (not server_proto_max) then + -- There is no info. Assume the best and act as if we would be compatible. + return true + end + return min_supp_proto <= server_proto_max and max_supp_proto >= server_proto_min +end + +function is_server_protocol_compat_or_error(server_proto_min, server_proto_max) + if not is_server_protocol_compat(server_proto_min, server_proto_max) then + local server_prot_ver_info, client_prot_ver_info + local s_p_min = server_proto_min + local s_p_max = server_proto_max + + if s_p_min ~= s_p_max then + server_prot_ver_info = fgettext_ne("Server supports protocol versions between $1 and $2. ", + s_p_min, s_p_max) + else + server_prot_ver_info = fgettext_ne("Server enforces protocol version $1. ", + s_p_min) + end + if min_supp_proto ~= max_supp_proto then + client_prot_ver_info= fgettext_ne("We support protocol versions between version $1 and $2.", + min_supp_proto, max_supp_proto) + else + client_prot_ver_info = fgettext_ne("We only support protocol version $1.", min_supp_proto) + end + gamedata.errormessage = fgettext_ne("Protocol version mismatch. ") + .. server_prot_ver_info + .. client_prot_ver_info + return false + end + + return true +end + +function menu_worldmt(selected, setting, value) + local world = menudata.worldlist:get_list()[selected] + if world then + local filename = world.path .. DIR_DELIM .. "world.mt" + local world_conf = Settings(filename) + + if value then + if not world_conf:write() then + core.log("error", "Failed to write world config file") + end + world_conf:set(setting, value) + world_conf:write() + else + return world_conf:get(setting) + end + else + return nil + end +end + +function menu_worldmt_legacy(selected) + local modes_names = {"creative_mode", "enable_damage", "server_announce"} + for _, mode_name in pairs(modes_names) do + local mode_val = menu_worldmt(selected, mode_name) + if mode_val then + core.settings:set(mode_name, mode_val) + else + menu_worldmt(selected, mode_name, core.settings:get(mode_name)) + end + end +end + +function confirmation_formspec(message, confirm_id, confirm_label, cancel_id, cancel_label) + return "size[10,2.5,true]" .. + "label[0.5,0.5;" .. message .. "]" .. + "style[" .. confirm_id .. ";bgcolor=red]" .. + "button[0.5,1.5;2.5,0.5;" .. confirm_id .. ";" .. confirm_label .. "]" .. + "button[7.0,1.5;2.5,0.5;" .. cancel_id .. ";" .. cancel_label .. "]" +end diff --git a/builtin/mainmenu/dlg_config_world.lua b/builtin/mainmenu/dlg_config_world.lua new file mode 100644 index 0000000..e76e10e --- /dev/null +++ b/builtin/mainmenu/dlg_config_world.lua @@ -0,0 +1,409 @@ +--Minetest +--Copyright (C) 2013 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-------------------------------------------------------------------------------- + +local enabled_all = false + +local function modname_valid(name) + return not name:find("[^a-z0-9_]") +end + +local function init_data(data) + data.list = filterlist.create( + pkgmgr.preparemodlist, + pkgmgr.comparemod, + function(element, uid) + if element.name == uid then + return true + end + end, + function(element, criteria) + if criteria.hide_game and + element.is_game_content then + return false + end + + if criteria.hide_modpackcontents and + element.modpack ~= nil then + return false + end + return true + end, + { + worldpath = data.worldspec.path, + gameid = data.worldspec.gameid + }) + + if data.selected_mod > data.list:size() then + data.selected_mod = 0 + end + + data.list:set_filtercriteria({ + hide_game = data.hide_gamemods, + hide_modpackcontents = data.hide_modpackcontents + }) + data.list:add_sort_mechanism("alphabetic", sort_mod_list) + data.list:set_sortmode("alphabetic") +end + + +-- Returns errors errors and a list of all enabled mods (inc. game and world mods) +-- +-- `with_errors` is a table from mod virtual path to `{ type = "error" | "warning" }`. +-- `enabled_mods_by_name` is a table from mod virtual path to `true`. +-- +-- @param world_path Path to the world +-- @param all_mods List of mods, with `enabled` property. +-- @returns with_errors, enabled_mods_by_name +local function check_mod_configuration(world_path, all_mods) + -- Build up lookup tables for enabled mods and all mods by vpath + local enabled_mod_paths = {} + local all_mods_by_vpath = {} + for _, mod in ipairs(all_mods) do + if mod.type == "mod" then + all_mods_by_vpath[mod.virtual_path] = mod + end + if mod.enabled then + enabled_mod_paths[mod.virtual_path] = mod.path + end + end + + -- Use the engine's mod configuration code to resolve dependencies and return any errors + local config_status = core.check_mod_configuration(world_path, enabled_mod_paths) + + -- Build the list of enabled mod virtual paths + local enabled_mods_by_name = {} + for _, mod in ipairs(config_status.satisfied_mods) do + assert(mod.virtual_path ~= "") + enabled_mods_by_name[mod.name] = all_mods_by_vpath[mod.virtual_path] or mod + end + for _, mod in ipairs(config_status.unsatisfied_mods) do + assert(mod.virtual_path ~= "") + enabled_mods_by_name[mod.name] = all_mods_by_vpath[mod.virtual_path] or mod + end + + -- Build the table of errors + local with_error = {} + for _, mod in ipairs(config_status.unsatisfied_mods) do + local error = { type = "warning" } + with_error[mod.virtual_path] = error + + for _, depname in ipairs(mod.unsatisfied_depends) do + if not enabled_mods_by_name[depname] then + error.type = "error" + break + end + end + end + + return with_error, enabled_mods_by_name +end + +local function get_formspec(data) + if not data.list then + init_data(data) + end + + local all_mods = data.list:get_list() + local with_error, enabled_mods_by_name = check_mod_configuration(data.worldspec.path, all_mods) + + local mod = all_mods[data.selected_mod] or {name = ""} + + local retval = + "size[11.5,7.5,true]" .. + "label[0.5,0;" .. fgettext("World:") .. "]" .. + "label[1.75,0;" .. data.worldspec.name .. "]" + + if mod.is_modpack or mod.type == "game" then + local info = core.formspec_escape( + core.get_content_info(mod.path).description) + if info == "" then + if mod.is_modpack then + info = fgettext("No modpack description provided.") + else + info = fgettext("No game description provided.") + end + end + retval = retval .. + "textarea[0.25,0.7;5.75,7.2;;" .. info .. ";]" + else + local hard_deps, soft_deps = pkgmgr.get_dependencies(mod.path) + + -- Add error messages to dep lists + if mod.enabled or mod.is_game_content then + for i, dep_name in ipairs(hard_deps) do + local dep = enabled_mods_by_name[dep_name] + if not dep then + hard_deps[i] = mt_color_red .. dep_name .. " " .. fgettext("(Unsatisfied)") + elseif with_error[dep.virtual_path] then + hard_deps[i] = mt_color_orange .. dep_name .. " " .. fgettext("(Enabled, has error)") + else + hard_deps[i] = mt_color_green .. dep_name + end + end + for i, dep_name in ipairs(soft_deps) do + local dep = enabled_mods_by_name[dep_name] + if dep and with_error[dep.virtual_path] then + soft_deps[i] = mt_color_orange .. dep_name .. " " .. fgettext("(Enabled, has error)") + elseif dep then + soft_deps[i] = mt_color_green .. dep_name + end + end + end + + local hard_deps_str = table.concat(hard_deps, ",") + local soft_deps_str = table.concat(soft_deps, ",") + + retval = retval .. + "label[0,0.7;" .. fgettext("Mod:") .. "]" .. + "label[0.75,0.7;" .. mod.name .. "]" + + if hard_deps_str == "" then + if soft_deps_str == "" then + retval = retval .. + "label[0,1.25;" .. + fgettext("No (optional) dependencies") .. "]" + else + retval = retval .. + "label[0,1.25;" .. fgettext("No hard dependencies") .. + "]" .. + "label[0,1.75;" .. fgettext("Optional dependencies:") .. + "]" .. + "textlist[0,2.25;5,4;world_config_optdepends;" .. + soft_deps_str .. ";0]" + end + else + if soft_deps_str == "" then + retval = retval .. + "label[0,1.25;" .. fgettext("Dependencies:") .. "]" .. + "textlist[0,1.75;5,4;world_config_depends;" .. + hard_deps_str .. ";0]" .. + "label[0,6;" .. fgettext("No optional dependencies") .. "]" + else + retval = retval .. + "label[0,1.25;" .. fgettext("Dependencies:") .. "]" .. + "textlist[0,1.75;5,2.125;world_config_depends;" .. + hard_deps_str .. ";0]" .. + "label[0,3.9;" .. fgettext("Optional dependencies:") .. + "]" .. + "textlist[0,4.375;5,1.8;world_config_optdepends;" .. + soft_deps_str .. ";0]" + end + end + end + + retval = retval .. + "button[3.25,7;2.5,0.5;btn_config_world_save;" .. + fgettext("Save") .. "]" .. + "button[5.75,7;2.5,0.5;btn_config_world_cancel;" .. + fgettext("Cancel") .. "]" .. + "button[9,7;2.5,0.5;btn_config_world_cdb;" .. + fgettext("Find More Mods") .. "]" + + if mod.name ~= "" and not mod.is_game_content then + if mod.is_modpack then + if pkgmgr.is_modpack_entirely_enabled(data, mod.name) then + retval = retval .. + "button[5.5,0.125;3,0.5;btn_mp_disable;" .. + fgettext("Disable modpack") .. "]" + else + retval = retval .. + "button[5.5,0.125;3,0.5;btn_mp_enable;" .. + fgettext("Enable modpack") .. "]" + end + else + retval = retval .. + "checkbox[5.5,-0.125;cb_mod_enable;" .. fgettext("enabled") .. + ";" .. tostring(mod.enabled) .. "]" + end + end + if enabled_all then + retval = retval .. + "button[8.95,0.125;2.5,0.5;btn_disable_all_mods;" .. + fgettext("Disable all") .. "]" + else + retval = retval .. + "button[8.95,0.125;2.5,0.5;btn_enable_all_mods;" .. + fgettext("Enable all") .. "]" + end + + local use_technical_names = core.settings:get_bool("show_technical_names") + + return retval .. + "tablecolumns[color;tree;image,align=inline,width=1.5,0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. + ",1=" .. core.formspec_escape(defaulttexturedir .. "checkbox_16_white.png") .. + ",2=" .. core.formspec_escape(defaulttexturedir .. "error_icon_orange.png") .. + ",3=" .. core.formspec_escape(defaulttexturedir .. "error_icon_red.png") .. ";text]" .. + "table[5.5,0.75;5.75,6;world_config_modlist;" .. + pkgmgr.render_packagelist(data.list, use_technical_names, with_error) .. ";" .. data.selected_mod .."]" +end + +local function handle_buttons(this, fields) + if fields.world_config_modlist then + local event = core.explode_table_event(fields.world_config_modlist) + this.data.selected_mod = event.row + core.settings:set("world_config_selected_mod", event.row) + + if event.type == "DCL" then + pkgmgr.enable_mod(this) + end + + return true + end + + if fields.key_enter then + pkgmgr.enable_mod(this) + return true + end + + if fields.cb_mod_enable ~= nil then + pkgmgr.enable_mod(this, core.is_yes(fields.cb_mod_enable)) + return true + end + + if fields.btn_mp_enable ~= nil or + fields.btn_mp_disable then + pkgmgr.enable_mod(this, fields.btn_mp_enable ~= nil) + return true + end + + if fields.btn_config_world_save then + local filename = this.data.worldspec.path .. DIR_DELIM .. "world.mt" + + local worldfile = Settings(filename) + local mods = worldfile:to_table() + + local rawlist = this.data.list:get_raw_list() + local was_set = {} + + for i = 1, #rawlist do + local mod = rawlist[i] + if not mod.is_modpack and + not mod.is_game_content then + if modname_valid(mod.name) then + if mod.enabled then + worldfile:set("load_mod_" .. mod.name, mod.virtual_path) + was_set[mod.name] = true + elseif not was_set[mod.name] then + worldfile:set("load_mod_" .. mod.name, "false") + end + elseif mod.enabled then + gamedata.errormessage = fgettext_ne("Failed to enable mo" .. + "d \"$1\" as it contains disallowed characters. " .. + "Only characters [a-z0-9_] are allowed.", + mod.name) + end + mods["load_mod_" .. mod.name] = nil + end + end + + -- Remove mods that are not present anymore + for key in pairs(mods) do + if key:sub(1, 9) == "load_mod_" then + worldfile:remove(key) + end + end + + if not worldfile:write() then + core.log("error", "Failed to write world config file") + end + + this:delete() + return true + end + + if fields.btn_config_world_cancel then + this:delete() + return true + end + + if fields.btn_config_world_cdb then + this.data.list = nil + + local dlg = create_store_dlg("mod") + dlg:set_parent(this) + this:hide() + dlg:show() + return true + end + + if fields.btn_enable_all_mods then + local list = this.data.list:get_raw_list() + + -- When multiple copies of a mod are installed, we need to avoid enabling multiple of them + -- at a time. So lets first collect all the enabled mods, and then use this to exclude + -- multiple enables. + + local was_enabled = {} + for i = 1, #list do + if not list[i].is_game_content + and not list[i].is_modpack and list[i].enabled then + was_enabled[list[i].name] = true + end + end + + for i = 1, #list do + if not list[i].is_game_content and not list[i].is_modpack and + not was_enabled[list[i].name] then + list[i].enabled = true + was_enabled[list[i].name] = true + end + end + + enabled_all = true + return true + end + + if fields.btn_disable_all_mods then + local list = this.data.list:get_raw_list() + + for i = 1, #list do + if not list[i].is_game_content + and not list[i].is_modpack then + list[i].enabled = false + end + end + enabled_all = false + return true + end + + return false +end + +function create_configure_world_dlg(worldidx) + local dlg = dialog_create("sp_config_world", get_formspec, handle_buttons) + + dlg.data.selected_mod = tonumber( + core.settings:get("world_config_selected_mod")) or 0 + + dlg.data.worldspec = core.get_worlds()[worldidx] + if not dlg.data.worldspec then + dlg:delete() + return + end + + dlg.data.worldconfig = pkgmgr.get_worldconfig(dlg.data.worldspec.path) + + if not dlg.data.worldconfig or not dlg.data.worldconfig.id or + dlg.data.worldconfig.id == "" then + dlg:delete() + return + end + + return dlg +end diff --git a/builtin/mainmenu/dlg_contentstore.lua b/builtin/mainmenu/dlg_contentstore.lua new file mode 100644 index 0000000..2152b8a --- /dev/null +++ b/builtin/mainmenu/dlg_contentstore.lua @@ -0,0 +1,1062 @@ +--Minetest +--Copyright (C) 2018-20 rubenwardy +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +if not core.get_http_api then + function create_store_dlg() + return messagebox("store", + fgettext("ContentDB is not available when Minetest was compiled without cURL")) + end + return +end + +-- Unordered preserves the original order of the ContentDB API, +-- before the package list is ordered based on installed state. +local store = { packages = {}, packages_full = {}, packages_full_unordered = {}, aliases = {} } + +local http = core.get_http_api() + +-- Screenshot +local screenshot_dir = core.get_cache_path() .. DIR_DELIM .. "cdb" +assert(core.create_dir(screenshot_dir)) +local screenshot_downloading = {} +local screenshot_downloaded = {} + +-- Filter +local search_string = "" +local cur_page = 1 +local num_per_page = 5 +local filter_type = 1 +local filter_types_titles = { + fgettext("All packages"), + fgettext("Games"), + fgettext("Mods"), + fgettext("Texture packs"), +} + +local number_downloading = 0 +local download_queue = {} + +local filter_types_type = { + nil, + "game", + "mod", + "txp", +} + +local REASON_NEW = "new" +local REASON_UPDATE = "update" +local REASON_DEPENDENCY = "dependency" + + +-- encodes for use as URL parameter or path component +local function urlencode(str) + return str:gsub("[^%a%d()._~-]", function(char) + return string.format("%%%02X", string.byte(char)) + end) +end +assert(urlencode("sample text?") == "sample%20text%3F") + + +local function get_download_url(package, reason) + local base_url = core.settings:get("contentdb_url") + local ret = base_url .. ("/packages/%s/releases/%d/download/"):format( + package.url_part, package.release) + if reason then + ret = ret .. "?reason=" .. reason + end + return ret +end + + +local function download_and_extract(param) + local package = param.package + + local filename = core.get_temp_path(true) + if filename == "" or not core.download_file(param.url, filename) then + core.log("error", "Downloading " .. dump(param.url) .. " failed") + return { + msg = fgettext("Failed to download $1", package.name) + } + end + + local tempfolder = core.get_temp_path() + if tempfolder ~= "" then + tempfolder = tempfolder .. DIR_DELIM .. "MT_" .. math.random(1, 1024000) + if not core.extract_zip(filename, tempfolder) then + tempfolder = nil + end + else + tempfolder = nil + end + os.remove(filename) + if not tempfolder then + return { + msg = fgettext("Install: Unsupported file type or broken archive"), + } + end + + return { + path = tempfolder + } +end + +local function start_install(package, reason) + local params = { + package = package, + url = get_download_url(package, reason), + } + + number_downloading = number_downloading + 1 + + local function callback(result) + if result.msg then + gamedata.errormessage = result.msg + else + local path, msg = pkgmgr.install_dir(package.type, result.path, package.name, package.path) + core.delete_dir(result.path) + if not path then + gamedata.errormessage = msg + else + core.log("action", "Installed package to " .. path) + + local conf_path + local name_is_title = false + if package.type == "mod" then + local actual_type = pkgmgr.get_folder_type(path) + if actual_type.type == "modpack" then + conf_path = path .. DIR_DELIM .. "modpack.conf" + else + conf_path = path .. DIR_DELIM .. "mod.conf" + end + elseif package.type == "game" then + conf_path = path .. DIR_DELIM .. "game.conf" + name_is_title = true + elseif package.type == "txp" then + conf_path = path .. DIR_DELIM .. "texture_pack.conf" + end + + if conf_path then + local conf = Settings(conf_path) + conf:set("title", package.title) + if not name_is_title then + conf:set("name", package.name) + end + if not conf:get("description") then + conf:set("description", package.short_description) + end + conf:set("author", package.author) + conf:set("release", package.release) + conf:write() + end + end + end + + package.downloading = false + + number_downloading = number_downloading - 1 + + local next = download_queue[1] + if next then + table.remove(download_queue, 1) + + start_install(next.package, next.reason) + end + + ui.update() + end + + package.queued = false + package.downloading = true + + if not core.handle_async(download_and_extract, params, callback) then + core.log("error", "ERROR: async event failed") + gamedata.errormessage = fgettext("Failed to download $1", package.name) + return + end +end + +local function queue_download(package, reason) + local max_concurrent_downloads = tonumber(core.settings:get("contentdb_max_concurrent_downloads")) + if number_downloading < math.max(max_concurrent_downloads, 1) then + start_install(package, reason) + else + table.insert(download_queue, { package = package, reason = reason }) + package.queued = true + end +end + +local function get_raw_dependencies(package) + if package.raw_deps then + return package.raw_deps + end + + local url_fmt = "/api/packages/%s/dependencies/?only_hard=1&protocol_version=%s&engine_version=%s" + local version = core.get_version() + local base_url = core.settings:get("contentdb_url") + local url = base_url .. url_fmt:format(package.url_part, core.get_max_supp_proto(), urlencode(version.string)) + + local response = http.fetch_sync({ url = url }) + if not response.succeeded then + return + end + + local data = core.parse_json(response.data) or {} + + local content_lookup = {} + for _, pkg in pairs(store.packages_full) do + content_lookup[pkg.id] = pkg + end + + for id, raw_deps in pairs(data) do + local package2 = content_lookup[id:lower()] + if package2 and not package2.raw_deps then + package2.raw_deps = raw_deps + + for _, dep in pairs(raw_deps) do + local packages = {} + for i=1, #dep.packages do + packages[#packages + 1] = content_lookup[dep.packages[i]:lower()] + end + dep.packages = packages + end + end + end + + return package.raw_deps +end + +local function has_hard_deps(raw_deps) + for i=1, #raw_deps do + if not raw_deps[i].is_optional then + return true + end + end + + return false +end + +-- Recursively resolve dependencies, given the installed mods +local function resolve_dependencies_2(raw_deps, installed_mods, out) + local function resolve_dep(dep) + -- Check whether it's already installed + if installed_mods[dep.name] then + return { + is_optional = dep.is_optional, + name = dep.name, + installed = true, + } + end + + -- Find exact name matches + local fallback + for _, package in pairs(dep.packages) do + if package.type ~= "game" then + if package.name == dep.name then + return { + is_optional = dep.is_optional, + name = dep.name, + installed = false, + package = package, + } + elseif not fallback then + fallback = package + end + end + end + + -- Otherwise, find the first mod that fulfils it + if fallback then + return { + is_optional = dep.is_optional, + name = dep.name, + installed = false, + package = fallback, + } + end + + return { + is_optional = dep.is_optional, + name = dep.name, + installed = false, + } + end + + for _, dep in pairs(raw_deps) do + if not dep.is_optional and not out[dep.name] then + local result = resolve_dep(dep) + out[dep.name] = result + if result and result.package and not result.installed then + local raw_deps2 = get_raw_dependencies(result.package) + if raw_deps2 then + resolve_dependencies_2(raw_deps2, installed_mods, out) + end + end + end + end + + return true +end + +-- Resolve dependencies for a package, calls the recursive version. +local function resolve_dependencies(raw_deps, game) + assert(game) + + local installed_mods = {} + + local mods = {} + pkgmgr.get_game_mods(game, mods) + for _, mod in pairs(mods) do + installed_mods[mod.name] = true + end + + for _, mod in pairs(pkgmgr.global_mods:get_list()) do + installed_mods[mod.name] = true + end + + local out = {} + if not resolve_dependencies_2(raw_deps, installed_mods, out) then + return nil + end + + local retval = {} + for _, dep in pairs(out) do + retval[#retval + 1] = dep + end + + table.sort(retval, function(a, b) + return a.name < b.name + end) + + return retval +end + +local install_dialog = {} +function install_dialog.get_formspec() + local package = install_dialog.package + local raw_deps = install_dialog.raw_deps + local will_install_deps = install_dialog.will_install_deps + + local selected_game_idx = 1 + local selected_gameid = core.settings:get("menu_last_game") + local games = table.copy(pkgmgr.games) + for i=1, #games do + if selected_gameid and games[i].id == selected_gameid then + selected_game_idx = i + end + + games[i] = core.formspec_escape(games[i].title) + end + + local selected_game = pkgmgr.games[selected_game_idx] + local deps_to_install = 0 + local deps_not_found = 0 + + install_dialog.dependencies = resolve_dependencies(raw_deps, selected_game) + local formatted_deps = {} + for _, dep in pairs(install_dialog.dependencies) do + formatted_deps[#formatted_deps + 1] = "#fff" + formatted_deps[#formatted_deps + 1] = core.formspec_escape(dep.name) + if dep.installed then + formatted_deps[#formatted_deps + 1] = "#ccf" + formatted_deps[#formatted_deps + 1] = fgettext("Already installed") + elseif dep.package then + formatted_deps[#formatted_deps + 1] = "#cfc" + formatted_deps[#formatted_deps + 1] = fgettext("$1 by $2", dep.package.title, dep.package.author) + deps_to_install = deps_to_install + 1 + else + formatted_deps[#formatted_deps + 1] = "#f00" + formatted_deps[#formatted_deps + 1] = fgettext("Not found") + deps_not_found = deps_not_found + 1 + end + end + + local message_bg = "#3333" + local message + if will_install_deps then + message = fgettext("$1 and $2 dependencies will be installed.", package.title, deps_to_install) + else + message = fgettext("$1 will be installed, and $2 dependencies will be skipped.", package.title, deps_to_install) + end + if deps_not_found > 0 then + message = fgettext("$1 required dependencies could not be found.", deps_not_found) .. + " " .. fgettext("Please check that the base game is correct.", deps_not_found) .. + "\n" .. message + message_bg = mt_color_orange + end + + local formspec = { + "formspec_version[3]", + "size[7,7.85]", + "style[title;border=false]", + "box[0,0;7,0.5;#3333]", + "button[0,0;7,0.5;title;", fgettext("Install $1", package.title) , "]", + + "container[0.375,0.70]", + + "label[0,0.25;", fgettext("Base Game:"), "]", + "dropdown[2,0;4.25,0.5;selected_game;", table.concat(games, ","), ";", selected_game_idx, "]", + + "label[0,0.8;", fgettext("Dependencies:"), "]", + + "tablecolumns[color;text;color;text]", + "table[0,1.1;6.25,3;packages;", table.concat(formatted_deps, ","), "]", + + "container_end[]", + + "checkbox[0.375,5.1;will_install_deps;", + fgettext("Install missing dependencies"), ";", + will_install_deps and "true" or "false", "]", + + "box[0,5.4;7,1.2;", message_bg, "]", + "textarea[0.375,5.5;6.25,1;;;", message, "]", + + "container[1.375,6.85]", + "button[0,0;2,0.8;install_all;", fgettext("Install"), "]", + "button[2.25,0;2,0.8;cancel;", fgettext("Cancel"), "]", + "container_end[]", + } + + return table.concat(formspec, "") +end + +function install_dialog.handle_submit(this, fields) + if fields.cancel then + this:delete() + return true + end + + if fields.will_install_deps ~= nil then + install_dialog.will_install_deps = core.is_yes(fields.will_install_deps) + return true + end + + if fields.install_all then + queue_download(install_dialog.package, REASON_NEW) + + if install_dialog.will_install_deps then + for _, dep in pairs(install_dialog.dependencies) do + if not dep.is_optional and not dep.installed and dep.package then + queue_download(dep.package, REASON_DEPENDENCY) + end + end + end + + this:delete() + return true + end + + if fields.selected_game then + for _, game in pairs(pkgmgr.games) do + if game.title == fields.selected_game then + core.settings:set("menu_last_game", game.id) + break + end + end + return true + end + + return false +end + +function install_dialog.create(package, raw_deps) + install_dialog.dependencies = nil + install_dialog.package = package + install_dialog.raw_deps = raw_deps + install_dialog.will_install_deps = true + return dialog_create("install_dialog", + install_dialog.get_formspec, + install_dialog.handle_submit, + nil) +end + + +local confirm_overwrite = {} +function confirm_overwrite.get_formspec() + local package = confirm_overwrite.package + + return confirmation_formspec( + fgettext("\"$1\" already exists. Would you like to overwrite it?", package.name), + 'install', fgettext("Overwrite"), + 'cancel', fgettext("Cancel")) +end + +function confirm_overwrite.handle_submit(this, fields) + if fields.cancel then + this:delete() + return true + end + + if fields.install then + this:delete() + confirm_overwrite.callback() + return true + end + + return false +end + +function confirm_overwrite.create(package, callback) + assert(type(package) == "table") + assert(type(callback) == "function") + + confirm_overwrite.package = package + confirm_overwrite.callback = callback + return dialog_create("confirm_overwrite", + confirm_overwrite.get_formspec, + confirm_overwrite.handle_submit, + nil) +end + + +local function get_file_extension(path) + local parts = path:split(".") + return parts[#parts] +end + +local function get_screenshot(package) + if not package.thumbnail then + return defaulttexturedir .. "no_screenshot.png" + elseif screenshot_downloading[package.thumbnail] then + return defaulttexturedir .. "loading_screenshot.png" + end + + -- Get tmp screenshot path + local ext = get_file_extension(package.thumbnail) + local filepath = screenshot_dir .. DIR_DELIM .. + ("%s-%s-%s.%s"):format(package.type, package.author, package.name, ext) + + -- Return if already downloaded + local file = io.open(filepath, "r") + if file then + file:close() + return filepath + end + + -- Show error if we've failed to download before + if screenshot_downloaded[package.thumbnail] then + return defaulttexturedir .. "error_screenshot.png" + end + + -- Download + + local function download_screenshot(params) + return core.download_file(params.url, params.dest) + end + local function callback(success) + screenshot_downloading[package.thumbnail] = nil + screenshot_downloaded[package.thumbnail] = true + if not success then + core.log("warning", "Screenshot download failed for some reason") + end + ui.update() + end + if core.handle_async(download_screenshot, + { dest = filepath, url = package.thumbnail }, callback) then + screenshot_downloading[package.thumbnail] = true + else + core.log("error", "ERROR: async event failed") + return defaulttexturedir .. "error_screenshot.png" + end + + return defaulttexturedir .. "loading_screenshot.png" +end + +function store.load() + local version = core.get_version() + local base_url = core.settings:get("contentdb_url") + local url = base_url .. + "/api/packages/?type=mod&type=game&type=txp&protocol_version=" .. + core.get_max_supp_proto() .. "&engine_version=" .. urlencode(version.string) + + for _, item in pairs(core.settings:get("contentdb_flag_blacklist"):split(",")) do + item = item:trim() + if item ~= "" then + url = url .. "&hide=" .. urlencode(item) + end + end + + local response = http.fetch_sync({ url = url }) + if not response.succeeded then + return + end + + store.packages_full = core.parse_json(response.data) or {} + store.aliases = {} + + for _, package in pairs(store.packages_full) do + local name_len = #package.name + -- This must match what store.update_paths() does! + package.id = package.author:lower() .. "/" + if package.type == "game" and name_len > 5 and package.name:sub(name_len - 4) == "_game" then + package.id = package.id .. package.name:sub(1, name_len - 5) + else + package.id = package.id .. package.name + end + + package.url_part = urlencode(package.author) .. "/" .. urlencode(package.name) + + if package.aliases then + for _, alias in ipairs(package.aliases) do + -- We currently don't support name changing + local suffix = "/" .. package.name + if alias:sub(-#suffix) == suffix then + store.aliases[alias:lower()] = package.id + end + end + end + end + + store.packages_full_unordered = store.packages_full + store.packages = store.packages_full + store.loaded = true +end + +function store.update_paths() + local mod_hash = {} + pkgmgr.refresh_globals() + for _, mod in pairs(pkgmgr.global_mods:get_list()) do + if mod.author and mod.release > 0 then + local id = mod.author:lower() .. "/" .. mod.name + mod_hash[store.aliases[id] or id] = mod + end + end + + local game_hash = {} + pkgmgr.update_gamelist() + for _, game in pairs(pkgmgr.games) do + if game.author ~= "" and game.release > 0 then + local id = game.author:lower() .. "/" .. game.id + game_hash[store.aliases[id] or id] = game + end + end + + local txp_hash = {} + for _, txp in pairs(pkgmgr.get_texture_packs()) do + if txp.author and txp.release > 0 then + local id = txp.author:lower() .. "/" .. txp.name + txp_hash[store.aliases[id] or id] = txp + end + end + + for _, package in pairs(store.packages_full) do + local content + if package.type == "mod" then + content = mod_hash[package.id] + elseif package.type == "game" then + content = game_hash[package.id] + elseif package.type == "txp" then + content = txp_hash[package.id] + end + + if content then + package.path = content.path + package.installed_release = content.release or 0 + else + package.path = nil + end + end +end + +function store.sort_packages() + local ret = {} + + -- Add installed content + for i=1, #store.packages_full_unordered do + local package = store.packages_full_unordered[i] + if package.path then + ret[#ret + 1] = package + end + end + + -- Sort installed content by title + table.sort(ret, function(a, b) + return a.title < b.title + end) + + -- Add uninstalled content + for i=1, #store.packages_full_unordered do + local package = store.packages_full_unordered[i] + if not package.path then + ret[#ret + 1] = package + end + end + + store.packages_full = ret +end + +function store.filter_packages(query) + if query == "" and filter_type == 1 then + store.packages = store.packages_full + return + end + + local keywords = {} + for word in query:lower():gmatch("%S+") do + table.insert(keywords, word) + end + + local function matches_keywords(package) + for k = 1, #keywords do + local keyword = keywords[k] + + if string.find(package.name:lower(), keyword, 1, true) or + string.find(package.title:lower(), keyword, 1, true) or + string.find(package.author:lower(), keyword, 1, true) or + string.find(package.short_description:lower(), keyword, 1, true) then + return true + end + end + + return false + end + + store.packages = {} + for _, package in pairs(store.packages_full) do + if (query == "" or matches_keywords(package)) and + (filter_type == 1 or package.type == filter_types_type[filter_type]) then + store.packages[#store.packages + 1] = package + end + end +end + +function store.get_formspec(dlgdata) + store.update_paths() + + dlgdata.pagemax = math.max(math.ceil(#store.packages / num_per_page), 1) + if cur_page > dlgdata.pagemax then + cur_page = 1 + end + + local W = 15.75 + local H = 9.5 + local formspec + if #store.packages_full > 0 then + formspec = { + "formspec_version[3]", + "size[15.75,9.5]", + "position[0.5,0.55]", + + "style[status,downloading,queued;border=false]", + + "container[0.375,0.375]", + "field[0,0;7.225,0.8;search_string;;", core.formspec_escape(search_string), "]", + "field_close_on_enter[search_string;false]", + "image_button[7.3,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "search.png"), ";search;]", + "image_button[8.125,0;0.8,0.8;", core.formspec_escape(defaulttexturedir .. "clear.png"), ";clear;]", + "dropdown[9.6,0;2.4,0.8;type;", table.concat(filter_types_titles, ","), ";", filter_type, "]", + "container_end[]", + + -- Page nav buttons + "container[0,", H - 0.8 - 0.375, "]", + "button[0.375,0;4,0.8;back;", fgettext("Back to Main Menu"), "]", + + "container[", W - 0.375 - 0.8*4 - 2, ",0]", + "image_button[0,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "start_icon.png;pstart;]", + "image_button[0.8,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "prev_icon.png;pback;]", + "style[pagenum;border=false]", + "button[1.6,0;2,0.8;pagenum;", tonumber(cur_page), " / ", tonumber(dlgdata.pagemax), "]", + "image_button[3.6,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "next_icon.png;pnext;]", + "image_button[4.4,0;0.8,0.8;", core.formspec_escape(defaulttexturedir), "end_icon.png;pend;]", + "container_end[]", + + "container_end[]", + } + + if number_downloading > 0 then + formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;downloading;" + if #download_queue > 0 then + formspec[#formspec + 1] = fgettext("$1 downloading,\n$2 queued", number_downloading, #download_queue) + else + formspec[#formspec + 1] = fgettext("$1 downloading...", number_downloading) + end + formspec[#formspec + 1] = "]" + else + local num_avail_updates = 0 + for i=1, #store.packages_full do + local package = store.packages_full[i] + if package.path and package.installed_release < package.release and + not (package.downloading or package.queued) then + num_avail_updates = num_avail_updates + 1 + end + end + + if num_avail_updates == 0 then + formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;status;" + formspec[#formspec + 1] = fgettext("No updates") + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "button[12.75,0.375;2.625,0.8;update_all;" + formspec[#formspec + 1] = fgettext("Update All [$1]", num_avail_updates) + formspec[#formspec + 1] = "]" + end + end + + if #store.packages == 0 then + formspec[#formspec + 1] = "label[4,3;" + formspec[#formspec + 1] = fgettext("No results") + formspec[#formspec + 1] = "]" + end + else + formspec = { + "size[12,7]", + "position[0.5,0.55]", + "label[4,3;", fgettext("No packages could be retrieved"), "]", + "container[0,", H - 0.8 - 0.375, "]", + "button[0,0;4,0.8;back;", fgettext("Back to Main Menu"), "]", + "container_end[]", + } + end + + -- download/queued tooltips always have the same message + local tooltip_colors = ";#dff6f5;#302c2e]" + formspec[#formspec + 1] = "tooltip[downloading;" .. fgettext("Downloading...") .. tooltip_colors + formspec[#formspec + 1] = "tooltip[queued;" .. fgettext("Queued") .. tooltip_colors + + local start_idx = (cur_page - 1) * num_per_page + 1 + for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do + local package = store.packages[i] + local container_y = (i - start_idx) * 1.375 + (2*0.375 + 0.8) + formspec[#formspec + 1] = "container[0.375," + formspec[#formspec + 1] = container_y + formspec[#formspec + 1] = "]" + + -- image + formspec[#formspec + 1] = "image[0,0;1.5,1;" + formspec[#formspec + 1] = core.formspec_escape(get_screenshot(package)) + formspec[#formspec + 1] = "]" + + -- title + formspec[#formspec + 1] = "label[1.875,0.1;" + formspec[#formspec + 1] = core.formspec_escape( + core.colorize(mt_color_green, package.title) .. + core.colorize("#BFBFBF", " by " .. package.author)) + formspec[#formspec + 1] = "]" + + -- buttons + local left_base = "image_button[-1.55,0;0.7,0.7;" .. core.formspec_escape(defaulttexturedir) + formspec[#formspec + 1] = "container[" + formspec[#formspec + 1] = W - 0.375*2 + formspec[#formspec + 1] = ",0.1]" + + if package.downloading then + formspec[#formspec + 1] = "animated_image[-1.7,-0.15;1,1;downloading;" + formspec[#formspec + 1] = core.formspec_escape(defaulttexturedir) + formspec[#formspec + 1] = "cdb_downloading.png;3;400;]" + elseif package.queued then + formspec[#formspec + 1] = left_base + formspec[#formspec + 1] = "cdb_queued.png;queued;]" + elseif not package.path then + local elem_name = "install_" .. i .. ";" + formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#71aa34]" + formspec[#formspec + 1] = left_base .. "cdb_add.png;" .. elem_name .. "]" + formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Install") .. tooltip_colors + else + if package.installed_release < package.release then + + -- The install_ action also handles updating + local elem_name = "install_" .. i .. ";" + formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#28ccdf]" + formspec[#formspec + 1] = left_base .. "cdb_update.png;" .. elem_name .. "]" + formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Update") .. tooltip_colors + else + + local elem_name = "uninstall_" .. i .. ";" + formspec[#formspec + 1] = "style[" .. elem_name .. "bgcolor=#a93b3b]" + formspec[#formspec + 1] = left_base .. "cdb_clear.png;" .. elem_name .. "]" + formspec[#formspec + 1] = "tooltip[" .. elem_name .. fgettext("Uninstall") .. tooltip_colors + end + end + + local web_elem_name = "view_" .. i .. ";" + formspec[#formspec + 1] = "image_button[-0.7,0;0.7,0.7;" .. + core.formspec_escape(defaulttexturedir) .. "cdb_viewonline.png;" .. web_elem_name .. "]" + formspec[#formspec + 1] = "tooltip[" .. web_elem_name .. + fgettext("View more information in a web browser") .. tooltip_colors + formspec[#formspec + 1] = "container_end[]" + + -- description + local description_width = W - 0.375*5 - 0.85 - 2*0.7 + formspec[#formspec + 1] = "textarea[1.855,0.3;" + formspec[#formspec + 1] = tostring(description_width) + formspec[#formspec + 1] = ",0.8;;;" + formspec[#formspec + 1] = core.formspec_escape(package.short_description) + formspec[#formspec + 1] = "]" + + formspec[#formspec + 1] = "container_end[]" + end + + return table.concat(formspec, "") +end + +function store.handle_submit(this, fields) + if fields.search or fields.key_enter_field == "search_string" then + search_string = fields.search_string:trim() + cur_page = 1 + store.filter_packages(search_string) + return true + end + + if fields.clear then + search_string = "" + cur_page = 1 + store.filter_packages("") + return true + end + + if fields.back then + this:delete() + return true + end + + if fields.pstart then + cur_page = 1 + return true + end + + if fields.pend then + cur_page = this.data.pagemax + return true + end + + if fields.pnext then + cur_page = cur_page + 1 + if cur_page > this.data.pagemax then + cur_page = 1 + end + return true + end + + if fields.pback then + if cur_page == 1 then + cur_page = this.data.pagemax + else + cur_page = cur_page - 1 + end + return true + end + + if fields.type then + local new_type = table.indexof(filter_types_titles, fields.type) + if new_type ~= filter_type then + filter_type = new_type + store.filter_packages(search_string) + return true + end + end + + if fields.update_all then + for i=1, #store.packages_full do + local package = store.packages_full[i] + if package.path and package.installed_release < package.release and + not (package.downloading or package.queued) then + queue_download(package, REASON_UPDATE) + end + end + return true + end + + local start_idx = (cur_page - 1) * num_per_page + 1 + assert(start_idx ~= nil) + for i=start_idx, math.min(#store.packages, start_idx+num_per_page-1) do + local package = store.packages[i] + assert(package) + + if fields["install_" .. i] then + local install_parent + if package.type == "mod" then + install_parent = core.get_modpath() + elseif package.type == "game" then + install_parent = core.get_gamepath() + elseif package.type == "txp" then + install_parent = core.get_texturepath() + else + error("Unknown package type: " .. package.type) + end + + + local function on_confirm() + local deps = get_raw_dependencies(package) + if deps and has_hard_deps(deps) then + local dlg = install_dialog.create(package, deps) + dlg:set_parent(this) + this:hide() + dlg:show() + else + queue_download(package, package.path and REASON_UPDATE or REASON_NEW) + end + end + + if not package.path and core.is_dir(install_parent .. DIR_DELIM .. package.name) then + local dlg = confirm_overwrite.create(package, on_confirm) + dlg:set_parent(this) + this:hide() + dlg:show() + else + on_confirm() + end + + return true + end + + if fields["uninstall_" .. i] then + local dlg = create_delete_content_dlg(package) + dlg:set_parent(this) + this:hide() + dlg:show() + return true + end + + if fields["view_" .. i] then + local url = ("%s/packages/%s?protocol_version=%d"):format( + core.settings:get("contentdb_url"), package.url_part, + core.get_max_supp_proto()) + core.open_url(url) + return true + end + end + + return false +end + +function create_store_dlg(type) + if not store.loaded or #store.packages_full == 0 then + store.load() + end + + store.update_paths() + store.sort_packages() + + search_string = "" + cur_page = 1 + + if type then + -- table.indexof does not work on tables that contain `nil` + for i, v in pairs(filter_types_type) do + if v == type then + filter_type = i + break + end + end + end + + store.filter_packages(search_string) + + return dialog_create("store", + store.get_formspec, + store.handle_submit, + nil) +end diff --git a/builtin/mainmenu/dlg_create_world.lua b/builtin/mainmenu/dlg_create_world.lua new file mode 100644 index 0000000..806e019 --- /dev/null +++ b/builtin/mainmenu/dlg_create_world.lua @@ -0,0 +1,488 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local function table_to_flags(ftable) + -- Convert e.g. { jungles = true, caves = false } to "jungles,nocaves" + local str = {} + for flag, is_set in pairs(ftable) do + str[#str + 1] = is_set and flag or ("no" .. flag) + end + return table.concat(str, ",") +end + +-- Same as check_flag but returns a string +local function strflag(flags, flag) + return (flags[flag] == true) and "true" or "false" +end + +local cb_caverns = { "caverns", fgettext("Caverns"), + fgettext("Very large caverns deep in the underground") } + +local flag_checkboxes = { + v5 = { + cb_caverns, + }, + v7 = { + cb_caverns, + { "ridges", fgettext("Rivers"), fgettext("Sea level rivers") }, + { "mountains", fgettext("Mountains") }, + { "floatlands", fgettext("Floatlands (experimental)"), + fgettext("Floating landmasses in the sky") }, + }, + carpathian = { + cb_caverns, + { "rivers", fgettext("Rivers"), fgettext("Sea level rivers") }, + }, + valleys = { + { "altitude_chill", fgettext("Altitude chill"), + fgettext("Reduces heat with altitude") }, + { "altitude_dry", fgettext("Altitude dry"), + fgettext("Reduces humidity with altitude") }, + { "humid_rivers", fgettext("Humid rivers"), + fgettext("Increases humidity around rivers") }, + { "vary_river_depth", fgettext("Vary river depth"), + fgettext("Low humidity and high heat causes shallow or dry rivers") }, + }, + flat = { + cb_caverns, + { "hills", fgettext("Hills") }, + { "lakes", fgettext("Lakes") }, + }, + fractal = { + { "terrain", fgettext("Additional terrain"), + fgettext("Generate non-fractal terrain: Oceans and underground") }, + }, + v6 = { + { "trees", fgettext("Trees and jungle grass") }, + { "flat", fgettext("Flat terrain") }, + { "mudflow", fgettext("Mud flow"), fgettext("Terrain surface erosion") }, + -- Biome settings are in mgv6_biomes below + }, +} + +local mgv6_biomes = { + { + fgettext("Temperate, Desert, Jungle, Tundra, Taiga"), + {jungles = true, snowbiomes = true} + }, + { + fgettext("Temperate, Desert, Jungle"), + {jungles = true, snowbiomes = false} + }, + { + fgettext("Temperate, Desert"), + {jungles = false, snowbiomes = false} + }, +} + +local function create_world_formspec(dialogdata) + + -- Point the player to ContentDB when no games are found + if #pkgmgr.games == 0 then + return "size[8,2.5,true]" .. + "style[label_button;border=false]" .. + "button[0.5,0.5;7,0.5;label_button;" .. + fgettext("You have no games installed.") .. "]" .. + "button[0.5,1.5;2.5,0.5;world_create_open_cdb;" .. fgettext("Install a game") .. "]" .. + "button[5.0,1.5;2.5,0.5;world_create_cancel;" .. fgettext("Cancel") .. "]" + end + + local current_mg = dialogdata.mg + local mapgens = core.get_mapgen_names() + + local gameid = core.settings:get("menu_last_game") + + local flags = dialogdata.flags + + local game = pkgmgr.find_by_gameid(gameid) + if game == nil then + -- should never happen but just pick the first game + game = pkgmgr.get_game(1) + core.settings:set("menu_last_game", game.id) + end + + local disallowed_mapgen_settings = {} + if game ~= nil then + local gameconfig = Settings(game.path.."/game.conf") + + local allowed_mapgens = (gameconfig:get("allowed_mapgens") or ""):split() + for key, value in pairs(allowed_mapgens) do + allowed_mapgens[key] = value:trim() + end + + local disallowed_mapgens = (gameconfig:get("disallowed_mapgens") or ""):split() + for key, value in pairs(disallowed_mapgens) do + disallowed_mapgens[key] = value:trim() + end + + if #allowed_mapgens > 0 then + for i = #mapgens, 1, -1 do + if table.indexof(allowed_mapgens, mapgens[i]) == -1 then + table.remove(mapgens, i) + end + end + end + + if #disallowed_mapgens > 0 then + for i = #mapgens, 1, -1 do + if table.indexof(disallowed_mapgens, mapgens[i]) > 0 then + table.remove(mapgens, i) + end + end + end + + local ds = (gameconfig:get("disallowed_mapgen_settings") or ""):split() + for _, value in pairs(ds) do + disallowed_mapgen_settings[value:trim()] = true + end + end + + local mglist = "" + local selindex + do -- build the list of mapgens + local i = 1 + local first_mg + for k, v in pairs(mapgens) do + if not first_mg then + first_mg = v + end + if current_mg == v then + selindex = i + end + i = i + 1 + mglist = mglist .. core.formspec_escape(v) .. "," + end + if not selindex then + selindex = 1 + current_mg = first_mg + end + mglist = mglist:sub(1, -2) + end + + -- The logic of the flag element IDs is as follows: + -- "flag_main_foo-bar-baz" controls dialogdata.flags["main"]["foo_bar_baz"] + -- see the buttonhandler for the implementation of this + + local mg_main_flags = function(mapgen, y) + if mapgen == "singlenode" then + return "", y + end + if disallowed_mapgen_settings["mg_flags"] then + return "", y + end + + local form = "checkbox[0," .. y .. ";flag_main_caves;" .. + fgettext("Caves") .. ";"..strflag(flags.main, "caves").."]" + y = y + 0.5 + + form = form .. "checkbox[0,"..y..";flag_main_dungeons;" .. + fgettext("Dungeons") .. ";"..strflag(flags.main, "dungeons").."]" + y = y + 0.5 + + local d_name = fgettext("Decorations") + local d_tt + if mapgen == "v6" then + d_tt = fgettext("Structures appearing on the terrain (no effect on trees and jungle grass created by v6)") + else + d_tt = fgettext("Structures appearing on the terrain, typically trees and plants") + end + form = form .. "checkbox[0,"..y..";flag_main_decorations;" .. + d_name .. ";" .. + strflag(flags.main, "decorations").."]" .. + "tooltip[flag_mg_decorations;" .. + d_tt .. + "]" + y = y + 0.5 + + form = form .. "tooltip[flag_main_caves;" .. + fgettext("Network of tunnels and caves") + .. "]" + return form, y + end + + local mg_specific_flags = function(mapgen, y) + if not flag_checkboxes[mapgen] then + return "", y + end + if disallowed_mapgen_settings["mg"..mapgen.."_spflags"] then + return "", y + end + local form = "" + for _, tab in pairs(flag_checkboxes[mapgen]) do + local id = "flag_"..mapgen.."_"..tab[1]:gsub("_", "-") + form = form .. ("checkbox[0,%f;%s;%s;%s]"): + format(y, id, tab[2], strflag(flags[mapgen], tab[1])) + + if tab[3] then + form = form .. "tooltip["..id..";"..tab[3].."]" + end + y = y + 0.5 + end + + if mapgen ~= "v6" then + -- No special treatment + return form, y + end + -- Special treatment for v6 (add biome widgets) + + -- Biome type (jungles, snowbiomes) + local biometype + if flags.v6.snowbiomes == true then + biometype = 1 + elseif flags.v6.jungles == true then + biometype = 2 + else + biometype = 3 + end + y = y + 0.3 + + form = form .. "label[0,"..(y+0.1)..";" .. fgettext("Biomes") .. "]" + y = y + 0.6 + + form = form .. "dropdown[0,"..y..";6.3;mgv6_biomes;" + for b=1, #mgv6_biomes do + form = form .. mgv6_biomes[b][1] + if b < #mgv6_biomes then + form = form .. "," + end + end + form = form .. ";" .. biometype.. "]" + + -- biomeblend + y = y + 0.55 + form = form .. "checkbox[0,"..y..";flag_v6_biomeblend;" .. + fgettext("Biome blending") .. ";"..strflag(flags.v6, "biomeblend").."]" .. + "tooltip[flag_v6_biomeblend;" .. + fgettext("Smooth transition between biomes") .. "]" + + return form, y + end + + local y_start = 0.0 + local y = y_start + local str_flags, str_spflags + local label_flags, label_spflags = "", "" + y = y + 0.3 + str_flags, y = mg_main_flags(current_mg, y) + if str_flags ~= "" then + label_flags = "label[0,"..y_start..";" .. fgettext("Mapgen flags") .. "]" + y_start = y + 0.4 + else + y_start = 0.0 + end + y = y_start + 0.3 + str_spflags = mg_specific_flags(current_mg, y) + if str_spflags ~= "" then + label_spflags = "label[0,"..y_start..";" .. fgettext("Mapgen-specific flags") .. "]" + end + + local retval = + "size[12.25,7,true]" .. + + -- Left side + "container[0,0]".. + "field[0.3,0.6;6,0.5;te_world_name;" .. + fgettext("World name") .. + ";" .. core.formspec_escape(dialogdata.worldname) .. "]" .. + "set_focus[te_world_name;false]" + + if not disallowed_mapgen_settings["seed"] then + + retval = retval .. "field[0.3,1.7;6,0.5;te_seed;" .. + fgettext("Seed") .. + ";".. core.formspec_escape(dialogdata.seed) .. "]" + + end + + retval = retval .. + "label[0,2;" .. fgettext("Mapgen") .. "]".. + "dropdown[0,2.5;6.3;dd_mapgen;" .. mglist .. ";" .. selindex .. "]" + + -- Warning if only devtest is installed + if #pkgmgr.games == 1 and pkgmgr.games[1].id == "devtest" then + retval = retval .. + "container[0,3.5]" .. + "box[0,0;5.8,1.7;#ff8800]" .. + "textarea[0.4,0.1;6,1.8;;;".. + fgettext("Development Test is meant for developers.") .. "]" .. + "button[1,1;4,0.5;world_create_open_cdb;" .. fgettext("Install another game") .. "]" .. + "container_end[]" + end + + retval = retval .. + "container_end[]" .. + + -- Right side + "container[6.2,0]".. + label_flags .. str_flags .. + label_spflags .. str_spflags .. + "container_end[]".. + + -- Menu buttons + "button[3.25,6.5;3,0.5;world_create_confirm;" .. fgettext("Create") .. "]" .. + "button[6.25,6.5;3,0.5;world_create_cancel;" .. fgettext("Cancel") .. "]" + + return retval + +end + +local function create_world_buttonhandler(this, fields) + + if fields["world_create_open_cdb"] then + local dlg = create_store_dlg("game") + dlg:set_parent(this.parent) + this:delete() + this.parent:hide() + dlg:show() + return true + end + + if fields["world_create_confirm"] or + fields["key_enter"] then + + local worldname = fields["te_world_name"] + local game, gameindex = pkgmgr.find_by_gameid(core.settings:get("menu_last_game")) + + local message + if game == nil then + message = fgettext("No game selected") + end + + if message == nil then + -- For unnamed worlds use the generated name 'world', + -- where the number increments: it is set to 1 larger than the largest + -- generated name number found. + if worldname == "" then + local worldnum_max = 0 + for _, world in ipairs(menudata.worldlist:get_list()) do + if world.name:match("^world%d+$") then + local worldnum = tonumber(world.name:sub(6)) + worldnum_max = math.max(worldnum_max, worldnum) + end + end + worldname = "world" .. worldnum_max + 1 + end + + if menudata.worldlist:uid_exists_raw(worldname) then + message = fgettext("A world named \"$1\" already exists", worldname) + end + end + + if message == nil then + this.data.seed = fields["te_seed"] or "" + this.data.mg = fields["dd_mapgen"] + + -- actual names as used by engine + local settings = { + fixed_map_seed = this.data.seed, + mg_name = this.data.mg, + mg_flags = table_to_flags(this.data.flags.main), + mgv5_spflags = table_to_flags(this.data.flags.v5), + mgv6_spflags = table_to_flags(this.data.flags.v6), + mgv7_spflags = table_to_flags(this.data.flags.v7), + mgfractal_spflags = table_to_flags(this.data.flags.fractal), + mgcarpathian_spflags = table_to_flags(this.data.flags.carpathian), + mgvalleys_spflags = table_to_flags(this.data.flags.valleys), + mgflat_spflags = table_to_flags(this.data.flags.flat), + } + message = core.create_world(worldname, gameindex, settings) + end + + if message == nil then + core.settings:set("menu_last_game", game.id) + menudata.worldlist:set_filtercriteria(game.id) + menudata.worldlist:refresh() + core.settings:set("mainmenu_last_selected_world", + menudata.worldlist:raw_index_by_uid(worldname)) + end + + gamedata.errormessage = message + this:delete() + return true + end + + this.data.worldname = fields["te_world_name"] + this.data.seed = fields["te_seed"] or "" + + if fields["games"] then + local gameindex = core.get_textlist_index("games") + core.settings:set("menu_last_game", pkgmgr.games[gameindex].id) + return true + end + + for k,v in pairs(fields) do + local split = string.split(k, "_", nil, 3) + if split and split[1] == "flag" then + -- We replaced the underscore of flag names with a dash. + local flag = string.gsub(split[3], "-", "_") + local ftable = this.data.flags[split[2]] + assert(ftable) + ftable[flag] = v == "true" + return true + end + end + + if fields["world_create_cancel"] then + this:delete() + return true + end + + if fields["mgv6_biomes"] then + local entry = core.formspec_escape(fields["mgv6_biomes"]) + for b=1, #mgv6_biomes do + if entry == mgv6_biomes[b][1] then + local ftable = this.data.flags.v6 + ftable.jungles = mgv6_biomes[b][2].jungles + ftable.snowbiomes = mgv6_biomes[b][2].snowbiomes + return true + end + end + end + + if fields["dd_mapgen"] then + this.data.mg = fields["dd_mapgen"] + return true + end + + return false +end + + +function create_create_world_dlg() + local retval = dialog_create("sp_create_world", + create_world_formspec, + create_world_buttonhandler, + nil) + retval.data = { + worldname = "", + -- settings the world is created with: + seed = core.settings:get("fixed_map_seed") or "", + mg = core.settings:get("mg_name"), + flags = { + main = core.settings:get_flags("mg_flags"), + v5 = core.settings:get_flags("mgv5_spflags"), + v6 = core.settings:get_flags("mgv6_spflags"), + v7 = core.settings:get_flags("mgv7_spflags"), + fractal = core.settings:get_flags("mgfractal_spflags"), + carpathian = core.settings:get_flags("mgcarpathian_spflags"), + valleys = core.settings:get_flags("mgvalleys_spflags"), + flat = core.settings:get_flags("mgflat_spflags"), + } + } + + return retval +end diff --git a/builtin/mainmenu/dlg_delete_content.lua b/builtin/mainmenu/dlg_delete_content.lua new file mode 100644 index 0000000..4463825 --- /dev/null +++ b/builtin/mainmenu/dlg_delete_content.lua @@ -0,0 +1,70 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-------------------------------------------------------------------------------- + +local function delete_content_formspec(dialogdata) + return confirmation_formspec( + fgettext("Are you sure you want to delete \"$1\"?", dialogdata.content.name), + 'dlg_delete_content_confirm', fgettext("Delete"), + 'dlg_delete_content_cancel', fgettext("Cancel")) +end + +-------------------------------------------------------------------------------- +local function delete_content_buttonhandler(this, fields) + if fields["dlg_delete_content_confirm"] ~= nil then + + if this.data.content.path ~= nil and + this.data.content.path ~= "" and + this.data.content.path ~= core.get_modpath() and + this.data.content.path ~= core.get_gamepath() and + this.data.content.path ~= core.get_texturepath() then + if not core.delete_dir(this.data.content.path) then + gamedata.errormessage = fgettext("pkgmgr: failed to delete \"$1\"", this.data.content.path) + end + + if this.data.content.type == "game" then + pkgmgr.update_gamelist() + else + pkgmgr.refresh_globals() + end + else + gamedata.errormessage = fgettext("pkgmgr: invalid path \"$1\"", this.data.content.path) + end + this:delete() + return true + end + + if fields["dlg_delete_content_cancel"] then + this:delete() + return true + end + + return false +end + +-------------------------------------------------------------------------------- +function create_delete_content_dlg(content) + assert(content.name) + + local retval = dialog_create("dlg_delete_content", + delete_content_formspec, + delete_content_buttonhandler, + nil) + retval.data.content = content + return retval +end diff --git a/builtin/mainmenu/dlg_delete_world.lua b/builtin/mainmenu/dlg_delete_world.lua new file mode 100644 index 0000000..67c0612 --- /dev/null +++ b/builtin/mainmenu/dlg_delete_world.lua @@ -0,0 +1,58 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +local function delete_world_formspec(dialogdata) + return confirmation_formspec( + fgettext("Delete World \"$1\"?", dialogdata.delete_name), + 'world_delete_confirm', fgettext("Delete"), + 'world_delete_cancel', fgettext("Cancel")) +end + +local function delete_world_buttonhandler(this, fields) + if fields["world_delete_confirm"] then + if this.data.delete_index > 0 and + this.data.delete_index <= #menudata.worldlist:get_raw_list() then + core.delete_world(this.data.delete_index) + menudata.worldlist:refresh() + end + this:delete() + return true + end + + if fields["world_delete_cancel"] then + this:delete() + return true + end + + return false +end + + +function create_delete_world_dlg(name_to_del, index_to_del) + assert(name_to_del ~= nil and type(name_to_del) == "string" and name_to_del ~= "") + assert(index_to_del ~= nil and type(index_to_del) == "number") + + local retval = dialog_create("delete_world", + delete_world_formspec, + delete_world_buttonhandler, + nil) + retval.data.delete_name = name_to_del + retval.data.delete_index = index_to_del + + return retval +end diff --git a/builtin/mainmenu/dlg_register.lua b/builtin/mainmenu/dlg_register.lua new file mode 100644 index 0000000..a765824 --- /dev/null +++ b/builtin/mainmenu/dlg_register.lua @@ -0,0 +1,123 @@ +--Minetest +--Copyright (C) 2022 rubenwardy +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-------------------------------------------------------------------------------- + +local function register_formspec(dialogdata) + local title = fgettext("Joining $1", dialogdata.server and dialogdata.server.name or dialogdata.address) + local buttons_y = 4 + 1.3 + if dialogdata.error then + buttons_y = buttons_y + 0.8 + end + + local retval = { + "formspec_version[4]", + "size[8,", tostring(buttons_y + 1.175), "]", + "set_focus[", (dialogdata.name ~= "" and "password" or "name"), "]", + "label[0.375,0.8;", title, "]", + "field[0.375,1.575;7.25,0.8;name;", core.formspec_escape(fgettext("Name")), ";", + core.formspec_escape(dialogdata.name), "]", + "pwdfield[0.375,2.875;7.25,0.8;password;", core.formspec_escape(fgettext("Password")), "]", + "pwdfield[0.375,4.175;7.25,0.8;password_2;", core.formspec_escape(fgettext("Confirm Password")), "]" + } + + if dialogdata.error then + table.insert_all(retval, { + "box[0.375,", tostring(buttons_y - 0.9), ";7.25,0.6;darkred]", + "label[0.625,", tostring(buttons_y - 0.6), ";", core.formspec_escape(dialogdata.error), "]", + }) + end + + table.insert_all(retval, { + "container[0.375,", tostring(buttons_y), "]", + "button[0,0;2.5,0.8;dlg_register_confirm;", fgettext("Register"), "]", + "button[4.75,0;2.5,0.8;dlg_register_cancel;", fgettext("Cancel"), "]", + "container_end[]", + }) + + return table.concat(retval, "") +end + +-------------------------------------------------------------------------------- +local function register_buttonhandler(this, fields) + this.data.name = fields.name + this.data.error = nil + + if fields.dlg_register_confirm or fields.key_enter then + if fields.name == "" then + this.data.error = fgettext("Missing name") + return true + end + if fields.password ~= fields.password_2 then + this.data.error = fgettext("Passwords do not match") + return true + end + + gamedata.playername = fields.name + gamedata.password = fields.password + gamedata.address = this.data.address + gamedata.port = this.data.port + gamedata.allow_login_or_register = "register" + gamedata.selected_world = 0 + + assert(gamedata.address and gamedata.port) + + local server = this.data.server + if server then + serverlistmgr.add_favorite(server) + gamedata.servername = server.name + gamedata.serverdescription = server.description + else + gamedata.servername = "" + gamedata.serverdescription = "" + + serverlistmgr.add_favorite({ + address = gamedata.address, + port = gamedata.port, + }) + end + + core.settings:set("name", fields.name) + core.settings:set("address", gamedata.address) + core.settings:set("remote_port", gamedata.port) + + core.start() + end + + if fields["dlg_register_cancel"] then + this:delete() + return true + end + + return false +end + +-------------------------------------------------------------------------------- +function create_register_dialog(address, port, server) + assert(address) + assert(type(port) == "number") + + local retval = dialog_create("dlg_register", + register_formspec, + register_buttonhandler, + nil) + retval.data.address = address + retval.data.port = port + retval.data.server = server + retval.data.name = core.settings:get("name") or "" + return retval +end diff --git a/builtin/mainmenu/dlg_rename_modpack.lua b/builtin/mainmenu/dlg_rename_modpack.lua new file mode 100644 index 0000000..ca76a8c --- /dev/null +++ b/builtin/mainmenu/dlg_rename_modpack.lua @@ -0,0 +1,73 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-------------------------------------------------------------------------------- + +local function rename_modpack_formspec(dialogdata) + local retval = + "size[11.5,4.5,true]" .. + "button[3.25,3.5;2.5,0.5;dlg_rename_modpack_confirm;".. + fgettext("Accept") .. "]" .. + "button[5.75,3.5;2.5,0.5;dlg_rename_modpack_cancel;".. + fgettext("Cancel") .. "]" + + local input_y = 2 + if dialogdata.mod.is_name_explicit then + retval = retval .. "textarea[1,0.2;10,2;;;" .. + fgettext("This modpack has an explicit name given in its modpack.conf " .. + "which will override any renaming here.") .. "]" + input_y = 2.5 + end + retval = retval .. + "field[2.5," .. input_y .. ";7,0.5;te_modpack_name;" .. + fgettext("Rename Modpack:") .. ";" .. dialogdata.mod.dir_name .. "]" + + return retval +end + +-------------------------------------------------------------------------------- +local function rename_modpack_buttonhandler(this, fields) + if fields["dlg_rename_modpack_confirm"] ~= nil then + local oldpath = this.data.mod.path + local targetpath = this.data.mod.parent_dir .. DIR_DELIM .. fields["te_modpack_name"] + os.rename(oldpath, targetpath) + pkgmgr.refresh_globals() + pkgmgr.selected_mod = pkgmgr.global_mods:get_current_index( + pkgmgr.global_mods:raw_index_by_uid(fields["te_modpack_name"])) + + this:delete() + return true + end + + if fields["dlg_rename_modpack_cancel"] then + this:delete() + return true + end + + return false +end + +-------------------------------------------------------------------------------- +function create_rename_modpack_dlg(modpack) + + local retval = dialog_create("dlg_delete_mod", + rename_modpack_formspec, + rename_modpack_buttonhandler, + nil) + retval.data.mod = modpack + return retval +end diff --git a/builtin/mainmenu/dlg_settings_advanced.lua b/builtin/mainmenu/dlg_settings_advanced.lua new file mode 100644 index 0000000..69562e6 --- /dev/null +++ b/builtin/mainmenu/dlg_settings_advanced.lua @@ -0,0 +1,1137 @@ +--Minetest +--Copyright (C) 2015 PilzAdam +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local FILENAME = "settingtypes.txt" + +local CHAR_CLASSES = { + SPACE = "[%s]", + VARIABLE = "[%w_%-%.]", + INTEGER = "[+-]?[%d]", + FLOAT = "[+-]?[%d%.]", + FLAGS = "[%w_%-%.,]", +} + +local function flags_to_table(flags) + return flags:gsub("%s+", ""):split(",", true) -- Remove all spaces and split +end + +-- returns error message, or nil +local function parse_setting_line(settings, line, read_all, base_level, allow_secure) + + -- strip carriage returns (CR, /r) + line = line:gsub("\r", "") + + -- comment + local comment = line:match("^#" .. CHAR_CLASSES.SPACE .. "*(.*)$") + if comment then + if settings.current_comment == "" then + settings.current_comment = comment + else + settings.current_comment = settings.current_comment .. "\n" .. comment + end + return + end + + -- clear current_comment so only comments directly above a setting are bound to it + -- but keep a local reference to it for variables in the current line + local current_comment = settings.current_comment + settings.current_comment = "" + + -- empty lines + if line:match("^" .. CHAR_CLASSES.SPACE .. "*$") then + return + end + + -- category + local stars, category = line:match("^%[([%*]*)([^%]]+)%]$") + if category then + table.insert(settings, { + name = category, + level = stars:len() + base_level, + type = "category", + }) + return + end + + -- settings + local first_part, name, readable_name, setting_type = line:match("^" + -- this first capture group matches the whole first part, + -- so we can later strip it from the rest of the line + .. "(" + .. "([" .. CHAR_CLASSES.VARIABLE .. "+)" -- variable name + .. CHAR_CLASSES.SPACE .. "*" + .. "%(([^%)]*)%)" -- readable name + .. CHAR_CLASSES.SPACE .. "*" + .. "(" .. CHAR_CLASSES.VARIABLE .. "+)" -- type + .. CHAR_CLASSES.SPACE .. "*" + .. ")") + + if not first_part then + return "Invalid line" + end + + if name:match("secure%.[.]*") and not allow_secure then + return "Tried to add \"secure.\" setting" + end + + if readable_name == "" then + readable_name = nil + end + local remaining_line = line:sub(first_part:len() + 1) + + if setting_type == "int" then + local default, min, max = remaining_line:match("^" + -- first int is required, the last 2 are optional + .. "(" .. CHAR_CLASSES.INTEGER .. "+)" .. CHAR_CLASSES.SPACE .. "*" + .. "(" .. CHAR_CLASSES.INTEGER .. "*)" .. CHAR_CLASSES.SPACE .. "*" + .. "(" .. CHAR_CLASSES.INTEGER .. "*)" + .. "$") + + if not default or not tonumber(default) then + return "Invalid integer setting" + end + + min = tonumber(min) + max = tonumber(max) + table.insert(settings, { + name = name, + readable_name = readable_name, + type = "int", + default = default, + min = min, + max = max, + comment = current_comment, + }) + return + end + + if setting_type == "string" + or setting_type == "key" or setting_type == "v3f" then + local default = remaining_line:match("^(.*)$") + + if not default then + return "Invalid string setting" + end + if setting_type == "key" and not read_all then + -- ignore key type if read_all is false + return + end + + table.insert(settings, { + name = name, + readable_name = readable_name, + type = setting_type, + default = default, + comment = current_comment, + }) + return + end + + if setting_type == "noise_params_2d" + or setting_type == "noise_params_3d" then + local default = remaining_line:match("^(.*)$") + + if not default then + return "Invalid string setting" + end + + local values = {} + local ti = 1 + local index = 1 + for match in default:gmatch("[+-]?[%d.-e]+") do -- All numeric characters + index = default:find("[+-]?[%d.-e]+", index) + match:len() + table.insert(values, match) + ti = ti + 1 + if ti > 9 then + break + end + end + index = default:find("[^, ]", index) + local flags = "" + if index then + flags = default:sub(index) + default = default:sub(1, index - 3) -- Make sure no flags in single-line format + end + table.insert(values, flags) + + table.insert(settings, { + name = name, + readable_name = readable_name, + type = setting_type, + default = default, + default_table = { + offset = values[1], + scale = values[2], + spread = { + x = values[3], + y = values[4], + z = values[5] + }, + seed = values[6], + octaves = values[7], + persistence = values[8], + lacunarity = values[9], + flags = values[10] + }, + values = values, + comment = current_comment, + noise_params = true, + flags = flags_to_table("defaults,eased,absvalue") + }) + return + end + + if setting_type == "bool" then + if remaining_line ~= "false" and remaining_line ~= "true" then + return "Invalid boolean setting" + end + + table.insert(settings, { + name = name, + readable_name = readable_name, + type = "bool", + default = remaining_line, + comment = current_comment, + }) + return + end + + if setting_type == "float" then + local default, min, max = remaining_line:match("^" + -- first float is required, the last 2 are optional + .. "(" .. CHAR_CLASSES.FLOAT .. "+)" .. CHAR_CLASSES.SPACE .. "*" + .. "(" .. CHAR_CLASSES.FLOAT .. "*)" .. CHAR_CLASSES.SPACE .. "*" + .. "(" .. CHAR_CLASSES.FLOAT .. "*)" + .."$") + + if not default or not tonumber(default) then + return "Invalid float setting" + end + + min = tonumber(min) + max = tonumber(max) + table.insert(settings, { + name = name, + readable_name = readable_name, + type = "float", + default = default, + min = min, + max = max, + comment = current_comment, + }) + return + end + + if setting_type == "enum" then + local default, values = remaining_line:match("^" + -- first value (default) may be empty (i.e. is optional) + .. "(" .. CHAR_CLASSES.VARIABLE .. "*)" .. CHAR_CLASSES.SPACE .. "*" + .. "(" .. CHAR_CLASSES.FLAGS .. "+)" + .. "$") + + if not default or values == "" then + return "Invalid enum setting" + end + + table.insert(settings, { + name = name, + readable_name = readable_name, + type = "enum", + default = default, + values = values:split(",", true), + comment = current_comment, + }) + return + end + + if setting_type == "path" or setting_type == "filepath" then + local default = remaining_line:match("^(.*)$") + + if not default then + return "Invalid path setting" + end + + table.insert(settings, { + name = name, + readable_name = readable_name, + type = setting_type, + default = default, + comment = current_comment, + }) + return + end + + if setting_type == "flags" then + local default, possible = remaining_line:match("^" + -- first value (default) may be empty (i.e. is optional) + -- this is implemented by making the last value optional, and + -- swapping them around if it turns out empty. + .. "(" .. CHAR_CLASSES.FLAGS .. "+)" .. CHAR_CLASSES.SPACE .. "*" + .. "(" .. CHAR_CLASSES.FLAGS .. "*)" + .. "$") + + if not default or not possible then + return "Invalid flags setting" + end + + if possible == "" then + possible = default + default = "" + end + + table.insert(settings, { + name = name, + readable_name = readable_name, + type = "flags", + default = default, + possible = flags_to_table(possible), + comment = current_comment, + }) + return + end + + return "Invalid setting type \"" .. setting_type .. "\"" +end + +local function parse_single_file(file, filepath, read_all, result, base_level, allow_secure) + -- store this helper variable in the table so it's easier to pass to parse_setting_line() + result.current_comment = "" + + local line = file:read("*line") + while line do + local error_msg = parse_setting_line(result, line, read_all, base_level, allow_secure) + if error_msg then + core.log("error", error_msg .. " in " .. filepath .. " \"" .. line .. "\"") + end + line = file:read("*line") + end + + result.current_comment = nil +end + +-- read_all: whether to ignore certain setting types for GUI or not +-- parse_mods: whether to parse settingtypes.txt in mods and games +local function parse_config_file(read_all, parse_mods) + local settings = {} + + do + local builtin_path = core.get_builtin_path() .. FILENAME + local file = io.open(builtin_path, "r") + if not file then + core.log("error", "Can't load " .. FILENAME) + return settings + end + + parse_single_file(file, builtin_path, read_all, settings, 0, true) + + file:close() + end + + if parse_mods then + -- Parse games + local games_category_initialized = false + local index = 1 + local game = pkgmgr.get_game(index) + while game do + local path = game.path .. DIR_DELIM .. FILENAME + local file = io.open(path, "r") + if file then + if not games_category_initialized then + fgettext_ne("Content: Games") -- not used, but needed for xgettext + table.insert(settings, { + name = "Content: Games", + level = 0, + type = "category", + }) + games_category_initialized = true + end + + table.insert(settings, { + name = game.name, + level = 1, + type = "category", + }) + + parse_single_file(file, path, read_all, settings, 2, false) + + file:close() + end + + index = index + 1 + game = pkgmgr.get_game(index) + end + + -- Parse mods + local mods_category_initialized = false + local mods = {} + get_mods(core.get_modpath(), "mods", mods) + for _, mod in ipairs(mods) do + local path = mod.path .. DIR_DELIM .. FILENAME + local file = io.open(path, "r") + if file then + if not mods_category_initialized then + fgettext_ne("Content: Mods") -- not used, but needed for xgettext + table.insert(settings, { + name = "Content: Mods", + level = 0, + type = "category", + }) + mods_category_initialized = true + end + + table.insert(settings, { + name = mod.name, + readable_name = mod.title, + level = 1, + type = "category", + }) + + parse_single_file(file, path, read_all, settings, 2, false) + + file:close() + end + end + + -- Parse client mods + local clientmods_category_initialized = false + local clientmods = {} + get_mods(core.get_clientmodpath(), "clientmods", clientmods) + for _, mod in ipairs(clientmods) do + local path = mod.path .. DIR_DELIM .. FILENAME + local file = io.open(path, "r") + if file then + if not clientmods_category_initialized then + fgettext_ne("Client Mods") -- not used, but needed for xgettext + table.insert(settings, { + name = "Client Mods", + level = 0, + type = "category", + }) + clientmods_category_initialized = true + end + + table.insert(settings, { + name = mod.name, + level = 1, + type = "category", + }) + + parse_single_file(file, path, read_all, settings, 2, false) + + file:close() + end + end + end + + return settings +end + +local function filter_settings(settings, searchstring) + if not searchstring or searchstring == "" then + return settings, -1 + end + + -- Setup the keyword list + local keywords = {} + for word in searchstring:lower():gmatch("%S+") do + table.insert(keywords, word) + end + + local result = {} + local category_stack = {} + local current_level = 0 + local best_setting = nil + for _, entry in pairs(settings) do + if entry.type == "category" then + -- Remove all settingless categories + while #category_stack > 0 and entry.level <= current_level do + table.remove(category_stack, #category_stack) + if #category_stack > 0 then + current_level = category_stack[#category_stack].level + else + current_level = 0 + end + end + + -- Push category onto stack + category_stack[#category_stack + 1] = entry + current_level = entry.level + else + -- See if setting matches keywords + local setting_score = 0 + for k = 1, #keywords do + local keyword = keywords[k] + + if string.find(entry.name:lower(), keyword, 1, true) then + setting_score = setting_score + 1 + end + + if entry.readable_name and + string.find(fgettext(entry.readable_name):lower(), keyword, 1, true) then + setting_score = setting_score + 1 + end + + if entry.comment and + string.find(fgettext_ne(entry.comment):lower(), keyword, 1, true) then + setting_score = setting_score + 1 + end + end + + -- Add setting to results if match + if setting_score > 0 then + -- Add parent categories + for _, category in pairs(category_stack) do + result[#result + 1] = category + end + category_stack = {} + + -- Add setting + result[#result + 1] = entry + entry.score = setting_score + + if not best_setting or + setting_score > result[best_setting].score then + best_setting = #result + end + end + end + end + return result, best_setting or -1 +end + +local full_settings = parse_config_file(false, true) +local search_string = "" +local settings = full_settings +local selected_setting = 1 + +local function get_current_value(setting) + local value = core.settings:get(setting.name) + if value == nil then + value = setting.default + end + return value +end + +local function get_current_np_group(setting) + local value = core.settings:get_np_group(setting.name) + if value == nil then + return setting.values + end + local p = "%g" + return { + p:format(value.offset), + p:format(value.scale), + p:format(value.spread.x), + p:format(value.spread.y), + p:format(value.spread.z), + p:format(value.seed), + p:format(value.octaves), + p:format(value.persistence), + p:format(value.lacunarity), + value.flags + } +end + +local function get_current_np_group_as_string(setting) + local value = core.settings:get_np_group(setting.name) + if value == nil then + return setting.default + end + return ("%g, %g, (%g, %g, %g), %g, %g, %g, %g"):format( + value.offset, + value.scale, + value.spread.x, + value.spread.y, + value.spread.z, + value.seed, + value.octaves, + value.persistence, + value.lacunarity + ) .. (value.flags ~= "" and (", " .. value.flags) or "") +end + +local checkboxes = {} -- handle checkboxes events + +local function create_change_setting_formspec(dialogdata) + local setting = settings[selected_setting] + -- Final formspec will be created at the end of this function + -- Default values below, may be changed depending on setting type + local width = 10 + local height = 3.5 + local description_height = 3 + local formspec = "" + + -- Setting-specific formspec elements + if setting.type == "bool" then + local selected_index = 1 + if core.is_yes(get_current_value(setting)) then + selected_index = 2 + end + formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;" + .. fgettext("Disabled") .. "," .. fgettext("Enabled") .. ";" + .. selected_index .. "]" + height = height + 1.25 + + elseif setting.type == "enum" then + local selected_index = 0 + formspec = "dropdown[3," .. height .. ";4,1;dd_setting_value;" + for index, value in ipairs(setting.values) do + -- translating value is not possible, since it's the value + -- that we set the setting to + formspec = formspec .. core.formspec_escape(value) .. "," + if get_current_value(setting) == value then + selected_index = index + end + end + if #setting.values > 0 then + formspec = formspec:sub(1, -2) -- remove trailing comma + end + formspec = formspec .. ";" .. selected_index .. "]" + height = height + 1.25 + + elseif setting.type == "path" or setting.type == "filepath" then + local current_value = dialogdata.selected_path + if not current_value then + current_value = get_current_value(setting) + end + formspec = "field[0.28," .. height + 0.15 .. ";8,1;te_setting_value;;" + .. core.formspec_escape(current_value) .. "]" + .. "button[8," .. height - 0.15 .. ";2,1;btn_browser_" + .. setting.type .. ";" .. fgettext("Browse") .. "]" + height = height + 1.15 + + elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then + local t = get_current_np_group(setting) + local dimension = 3 + if setting.type == "noise_params_2d" then + dimension = 2 + end + + -- More space for 3x3 fields + description_height = description_height - 1.5 + height = height - 1.5 + + local fields = {} + local function add_field(x, name, label, value) + fields[#fields + 1] = ("field[%f,%f;3.3,1;%s;%s;%s]"):format( + x, height, name, label, core.formspec_escape(value or "") + ) + end + -- First row + height = height + 0.3 + add_field(0.3, "te_offset", fgettext("Offset"), t[1]) + add_field(3.6, "te_scale", fgettext("Scale"), t[2]) + add_field(6.9, "te_seed", fgettext("Seed"), t[6]) + height = height + 1.1 + + -- Second row + add_field(0.3, "te_spreadx", fgettext("X spread"), t[3]) + if dimension == 3 then + add_field(3.6, "te_spready", fgettext("Y spread"), t[4]) + else + fields[#fields + 1] = "label[4," .. height - 0.2 .. ";" .. + fgettext("2D Noise") .. "]" + end + add_field(6.9, "te_spreadz", fgettext("Z spread"), t[5]) + height = height + 1.1 + + -- Third row + add_field(0.3, "te_octaves", fgettext("Octaves"), t[7]) + add_field(3.6, "te_persist", fgettext("Persistence"), t[8]) + add_field(6.9, "te_lacun", fgettext("Lacunarity"), t[9]) + height = height + 1.1 + + + local enabled_flags = flags_to_table(t[10]) + local flags = {} + for _, name in ipairs(enabled_flags) do + -- Index by name, to avoid iterating over all enabled_flags for every possible flag. + flags[name] = true + end + for _, name in ipairs(setting.flags) do + local checkbox_name = "cb_" .. name + local is_enabled = flags[name] == true -- to get false if nil + checkboxes[checkbox_name] = is_enabled + end + -- Flags + formspec = table.concat(fields) + .. "checkbox[0.5," .. height - 0.6 .. ";cb_defaults;" + --[[~ "defaults" is a noise parameter flag. + It describes the default processing options + for noise settings in main menu -> "All Settings". ]] + .. fgettext("defaults") .. ";" -- defaults + .. tostring(flags["defaults"] == true) .. "]" -- to get false if nil + .. "checkbox[5," .. height - 0.6 .. ";cb_eased;" + --[[~ "eased" is a noise parameter flag. + It is used to make the map smoother and + can be enabled in noise settings in + main menu -> "All Settings". ]] + .. fgettext("eased") .. ";" -- eased + .. tostring(flags["eased"] == true) .. "]" + .. "checkbox[5," .. height - 0.15 .. ";cb_absvalue;" + --[[~ "absvalue" is a noise parameter flag. + It is short for "absolute value". + It can be enabled in noise settings in + main menu -> "All Settings". ]] + .. fgettext("absvalue") .. ";" -- absvalue + .. tostring(flags["absvalue"] == true) .. "]" + height = height + 1 + + elseif setting.type == "v3f" then + local val = get_current_value(setting) + local v3f = {} + for line in val:gmatch("[+-]?[%d.+-eE]+") do -- All numeric characters + table.insert(v3f, line) + end + + height = height + 0.3 + formspec = formspec + .. "field[0.3," .. height .. ";3.3,1;te_x;" + .. fgettext("X") .. ";" -- X + .. core.formspec_escape(v3f[1] or "") .. "]" + .. "field[3.6," .. height .. ";3.3,1;te_y;" + .. fgettext("Y") .. ";" -- Y + .. core.formspec_escape(v3f[2] or "") .. "]" + .. "field[6.9," .. height .. ";3.3,1;te_z;" + .. fgettext("Z") .. ";" -- Z + .. core.formspec_escape(v3f[3] or "") .. "]" + height = height + 1.1 + + elseif setting.type == "flags" then + local current_flags = flags_to_table(get_current_value(setting)) + local flags = {} + for _, name in ipairs(current_flags) do + -- Index by name, to avoid iterating over all enabled_flags for every possible flag. + if name:sub(1, 2) == "no" then + flags[name:sub(3)] = false + else + flags[name] = true + end + end + local flags_count = #setting.possible / 2 + local max_height = math.ceil(flags_count / 2) / 2 + + -- More space for flags + description_height = description_height - 1 + height = height - 1 + + local fields = {} -- To build formspec + local j = 1 + for _, name in ipairs(setting.possible) do + if name:sub(1, 2) ~= "no" then + local x = 0.5 + local y = height + j / 2 - 0.75 + if j - 1 >= flags_count / 2 then -- 2nd column + x = 5 + y = y - max_height + end + j = j + 1; + local checkbox_name = "cb_" .. name + local is_enabled = flags[name] == true -- to get false if nil + checkboxes[checkbox_name] = is_enabled + + fields[#fields + 1] = ("checkbox[%f,%f;%s;%s;%s]"):format( + x, y, checkbox_name, name, tostring(is_enabled) + ) + end + end + formspec = table.concat(fields) + height = height + max_height + 0.25 + + else + -- TODO: fancy input for float, int + local text = get_current_value(setting) + if dialogdata.error_message and dialogdata.entered_text then + text = dialogdata.entered_text + end + formspec = "field[0.28," .. height + 0.15 .. ";" .. width .. ",1;te_setting_value;;" + .. core.formspec_escape(text) .. "]" + height = height + 1.15 + end + + -- Box good, textarea bad. Calculate textarea size from box. + local function create_textfield(size, label, text, bg_color) + local textarea = { + x = size.x + 0.3, + y = size.y, + w = size.w + 0.25, + h = size.h * 1.16 + 0.12 + } + return ("box[%f,%f;%f,%f;%s]textarea[%f,%f;%f,%f;;%s;%s]"):format( + size.x, size.y, size.w, size.h, bg_color or "#000", + textarea.x, textarea.y, textarea.w, textarea.h, + core.formspec_escape(label), core.formspec_escape(text) + ) + + end + + -- When there's an error: Shrink description textarea and add error below + if dialogdata.error_message then + local error_box = { + x = 0, + y = description_height - 0.4, + w = width - 0.25, + h = 0.5 + } + formspec = formspec .. + create_textfield(error_box, "", dialogdata.error_message, "#600") + description_height = description_height - 0.75 + end + + -- Get description field + local description_box = { + x = 0, + y = 0.2, + w = width - 0.25, + h = description_height + } + + local setting_name = setting.name + if setting.readable_name then + setting_name = fgettext_ne(setting.readable_name) .. + " (" .. setting.name .. ")" + end + + local comment_text + if setting.comment == "" then + comment_text = fgettext_ne("(No description of setting given)") + else + comment_text = fgettext_ne(setting.comment) + end + + return ( + "size[" .. width .. "," .. height + 0.25 .. ",true]" .. + create_textfield(description_box, setting_name, comment_text) .. + formspec .. + "button[" .. width / 2 - 2.5 .. "," .. height - 0.4 .. ";2.5,1;btn_done;" .. + fgettext("Save") .. "]" .. + "button[" .. width / 2 .. "," .. height - 0.4 .. ";2.5,1;btn_cancel;" .. + fgettext("Cancel") .. "]" + ) +end + +local function handle_change_setting_buttons(this, fields) + local setting = settings[selected_setting] + if fields["btn_done"] or fields["key_enter"] then + if setting.type == "bool" then + local new_value = fields["dd_setting_value"] + -- Note: new_value is the actual (translated) value shown in the dropdown + core.settings:set_bool(setting.name, new_value == fgettext("Enabled")) + + elseif setting.type == "enum" then + local new_value = fields["dd_setting_value"] + core.settings:set(setting.name, new_value) + + elseif setting.type == "int" then + local new_value = tonumber(fields["te_setting_value"]) + if not new_value or math.floor(new_value) ~= new_value then + this.data.error_message = fgettext_ne("Please enter a valid integer.") + this.data.entered_text = fields["te_setting_value"] + core.update_formspec(this:get_formspec()) + return true + end + if setting.min and new_value < setting.min then + this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min) + this.data.entered_text = fields["te_setting_value"] + core.update_formspec(this:get_formspec()) + return true + end + if setting.max and new_value > setting.max then + this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max) + this.data.entered_text = fields["te_setting_value"] + core.update_formspec(this:get_formspec()) + return true + end + core.settings:set(setting.name, new_value) + + elseif setting.type == "float" then + local new_value = tonumber(fields["te_setting_value"]) + if not new_value then + this.data.error_message = fgettext_ne("Please enter a valid number.") + this.data.entered_text = fields["te_setting_value"] + core.update_formspec(this:get_formspec()) + return true + end + if setting.min and new_value < setting.min then + this.data.error_message = fgettext_ne("The value must be at least $1.", setting.min) + this.data.entered_text = fields["te_setting_value"] + core.update_formspec(this:get_formspec()) + return true + end + if setting.max and new_value > setting.max then + this.data.error_message = fgettext_ne("The value must not be larger than $1.", setting.max) + this.data.entered_text = fields["te_setting_value"] + core.update_formspec(this:get_formspec()) + return true + end + core.settings:set(setting.name, new_value) + + elseif setting.type == "flags" then + local values = {} + for _, name in ipairs(setting.possible) do + if name:sub(1, 2) ~= "no" then + if checkboxes["cb_" .. name] then + table.insert(values, name) + else + table.insert(values, "no" .. name) + end + end + end + + checkboxes = {} + + local new_value = table.concat(values, ", ") + core.settings:set(setting.name, new_value) + + elseif setting.type == "noise_params_2d" or setting.type == "noise_params_3d" then + local np_flags = {} + for _, name in ipairs(setting.flags) do + if checkboxes["cb_" .. name] then + table.insert(np_flags, name) + end + end + + checkboxes = {} + + if setting.type == "noise_params_2d" then + fields["te_spready"] = fields["te_spreadz"] + end + local new_value = { + offset = fields["te_offset"], + scale = fields["te_scale"], + spread = { + x = fields["te_spreadx"], + y = fields["te_spready"], + z = fields["te_spreadz"] + }, + seed = fields["te_seed"], + octaves = fields["te_octaves"], + persistence = fields["te_persist"], + lacunarity = fields["te_lacun"], + flags = table.concat(np_flags, ", ") + } + core.settings:set_np_group(setting.name, new_value) + + elseif setting.type == "v3f" then + local new_value = "(" + .. fields["te_x"] .. ", " + .. fields["te_y"] .. ", " + .. fields["te_z"] .. ")" + core.settings:set(setting.name, new_value) + + else + local new_value = fields["te_setting_value"] + core.settings:set(setting.name, new_value) + end + core.settings:write() + this:delete() + return true + end + + if fields["btn_cancel"] then + this:delete() + return true + end + + if fields["btn_browser_path"] then + core.show_path_select_dialog("dlg_browse_path", + fgettext_ne("Select directory"), false) + end + + if fields["btn_browser_filepath"] then + core.show_path_select_dialog("dlg_browse_path", + fgettext_ne("Select file"), true) + end + + if fields["dlg_browse_path_accepted"] then + this.data.selected_path = fields["dlg_browse_path_accepted"] + core.update_formspec(this:get_formspec()) + end + + if setting.type == "flags" + or setting.type == "noise_params_2d" + or setting.type == "noise_params_3d" then + for name, value in pairs(fields) do + if name:sub(1, 3) == "cb_" then + checkboxes[name] = value == "true" + end + end + end + + return false +end + +local function create_settings_formspec(tabview, _, tabdata) + local formspec = "size[12,5.4;true]" .. + "tablecolumns[color;tree;text,width=28;text]" .. + "tableoptions[background=#00000000;border=false]" .. + "field[0.3,0.1;10.2,1;search_string;;" .. core.formspec_escape(search_string) .. "]" .. + "field_close_on_enter[search_string;false]" .. + "button[10.2,-0.2;2,1;search;" .. fgettext("Search") .. "]" .. + "table[0,0.8;12,3.5;list_settings;" + + local current_level = 0 + for _, entry in ipairs(settings) do + local name + if not core.settings:get_bool("show_technical_names") and entry.readable_name then + name = fgettext_ne(entry.readable_name) + else + name = entry.name + end + + if entry.type == "category" then + current_level = entry.level + formspec = formspec .. "#FFFF00," .. current_level .. "," .. fgettext(name) .. ",," + + elseif entry.type == "bool" then + local value = get_current_value(entry) + if core.is_yes(value) then + value = fgettext("Enabled") + else + value = fgettext("Disabled") + end + formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. "," + .. value .. "," + + elseif entry.type == "key" then --luacheck: ignore + -- ignore key settings, since we have a special dialog for them + + elseif entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then + formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. "," + .. core.formspec_escape(get_current_np_group_as_string(entry)) .. "," + + else + formspec = formspec .. "," .. (current_level + 1) .. "," .. core.formspec_escape(name) .. "," + .. core.formspec_escape(get_current_value(entry)) .. "," + end + end + + if #settings > 0 then + formspec = formspec:sub(1, -2) -- remove trailing comma + end + formspec = formspec .. ";" .. selected_setting .. "]" .. + "button[0,4.9;4,1;btn_back;".. fgettext("< Back to Settings page") .. "]" .. + "button[10,4.9;2,1;btn_edit;" .. fgettext("Edit") .. "]" .. + "button[7,4.9;3,1;btn_restore;" .. fgettext("Restore Default") .. "]" .. + "checkbox[0,4.3;cb_tech_settings;" .. fgettext("Show technical names") .. ";" + .. dump(core.settings:get_bool("show_technical_names")) .. "]" + + return formspec +end + +local function handle_settings_buttons(this, fields, tabname, tabdata) + local list_enter = false + if fields["list_settings"] then + selected_setting = core.get_table_index("list_settings") + if core.explode_table_event(fields["list_settings"]).type == "DCL" then + -- Directly toggle booleans + local setting = settings[selected_setting] + if setting and setting.type == "bool" then + local current_value = get_current_value(setting) + core.settings:set_bool(setting.name, not core.is_yes(current_value)) + core.settings:write() + return true + else + list_enter = true + end + else + return true + end + end + + if fields.search or fields.key_enter_field == "search_string" then + if search_string == fields.search_string then + if selected_setting > 0 then + -- Go to next result on enter press + local i = selected_setting + 1 + local looped = false + while i > #settings or settings[i].type == "category" do + i = i + 1 + if i > #settings then + -- Stop infinte looping + if looped then + return false + end + i = 1 + looped = true + end + end + selected_setting = i + core.update_formspec(this:get_formspec()) + return true + end + else + -- Search for setting + search_string = fields.search_string + settings, selected_setting = filter_settings(full_settings, search_string) + core.update_formspec(this:get_formspec()) + end + return true + end + + if fields["btn_edit"] or list_enter then + local setting = settings[selected_setting] + if setting and setting.type ~= "category" then + local edit_dialog = dialog_create("change_setting", + create_change_setting_formspec, handle_change_setting_buttons) + edit_dialog:set_parent(this) + this:hide() + edit_dialog:show() + end + return true + end + + if fields["btn_restore"] then + local setting = settings[selected_setting] + if setting and setting.type ~= "category" then + core.settings:remove(setting.name) + core.settings:write() + core.update_formspec(this:get_formspec()) + end + return true + end + + if fields["btn_back"] then + this:delete() + return true + end + + if fields["cb_tech_settings"] then + core.settings:set("show_technical_names", fields["cb_tech_settings"]) + core.settings:write() + core.update_formspec(this:get_formspec()) + return true + end + + return false +end + +function create_adv_settings_dlg() + local dlg = dialog_create("settings_advanced", + create_settings_formspec, + handle_settings_buttons, + nil) + + return dlg +end + +-- Uncomment to generate 'minetest.conf.example' and 'settings_translation_file.cpp'. +-- For RUN_IN_PLACE the generated files may appear in the 'bin' folder. +-- See comment and alternative line at the end of 'generate_from_settingtypes.lua'. + +--assert(loadfile(core.get_builtin_path().."mainmenu"..DIR_DELIM.. +-- "generate_from_settingtypes.lua"))(parse_config_file(true, false)) diff --git a/builtin/mainmenu/dlg_version_info.lua b/builtin/mainmenu/dlg_version_info.lua new file mode 100644 index 0000000..568fca3 --- /dev/null +++ b/builtin/mainmenu/dlg_version_info.lua @@ -0,0 +1,172 @@ +--[[ +Minetest +Copyright (C) 2018-2020 SmallJoker, 2022 rubenwardy + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +]] + +if not core.get_http_api then + function check_new_version() + end + return +end + +local function version_info_formspec(data) + local cur_ver = core.get_version() + local title = fgettext("A new $1 version is available", cur_ver.project) + local message = + fgettext("Installed version: $1\nNew version: $2\n" .. + "Visit $3 to find out how to get the newest version and stay up to date" .. + " with features and bugfixes.", + cur_ver.string, data.new_version or "", data.url or "") + + local fs = { + "formspec_version[3]", + "size[12.8,7]", + "style_type[label;textcolor=#0E0]", + "label[0.5,0.8;", core.formspec_escape(title), "]", + "textarea[0.4,1.6;12,3.4;;;", + core.formspec_escape(message), "]", + "container[0.4,5.8]", + "button[0.0,0;4.0,0.8;version_check_visit;", fgettext("Visit website"), "]", + "button[4.5,0;3.5,0.8;version_check_remind;", fgettext("Later"), "]", + "button[8.5.5,0;3.5,0.8;version_check_never;", fgettext("Never"), "]", + "container_end[]", + } + + return table.concat(fs, "") +end + +local function version_info_buttonhandler(this, fields) + if fields.version_check_remind then + -- Erase last known, user will be reminded again at next check + core.settings:set("update_last_known", "") + this:delete() + return true + end + if fields.version_check_never then + core.settings:set("update_last_checked", "disabled") + this:delete() + return true + end + if fields.version_check_visit then + if type(this.data.url) == "string" then + core.open_url(this.data.url) + end + this:delete() + return true + end + + return false +end + +local function create_version_info_dlg(new_version, url) + assert(type(new_version) == "string") + assert(type(url) == "string") + + local retval = dialog_create("version_info", + version_info_formspec, + version_info_buttonhandler, + nil) + + retval.data.new_version = new_version + retval.data.url = url + + return retval +end + +local function get_current_version_code() + -- Format: Major.Minor.Patch + -- Convert to MMMNNNPPP + local cur_string = core.get_version().string + local cur_major, cur_minor, cur_patch = cur_string:match("^(%d+).(%d+).(%d+)") + + if not cur_patch then + core.log("error", "Failed to parse version numbers (invalid tag format?)") + return + end + + return (cur_major * 1000 + cur_minor) * 1000 + cur_patch +end + +local function on_version_info_received(json) + local maintab = ui.find_by_name("maintab") + if maintab.hidden then + -- Another dialog is open, abort. + return + end + + local known_update = tonumber(core.settings:get("update_last_known")) or 0 + + -- Format: MMNNPPP (Major, Minor, Patch) + local new_number = type(json.latest) == "table" and json.latest.version_code + if type(new_number) ~= "number" then + core.log("error", "Failed to read version number (invalid response?)") + return + end + + local cur_number = get_current_version_code() + if new_number <= known_update or new_number < cur_number then + return + end + + -- Also consider updating from 1.2.3-dev to 1.2.3 + if new_number == cur_number and not core.get_version().is_dev then + return + end + + core.settings:set("update_last_known", tostring(new_number)) + + -- Show version info dialog (once) + maintab:hide() + + local version_info_dlg = create_version_info_dlg(json.latest.version, json.latest.url) + version_info_dlg:set_parent(maintab) + version_info_dlg:show() + + ui.update() +end + +function check_new_version() + local url = core.settings:get("update_information_url") + if core.settings:get("update_last_checked") == "disabled" or + url == "" then + -- Never show any updates + return + end + + local time_now = os.time() + local time_checked = tonumber(core.settings:get("update_last_checked")) or 0 + if time_now - time_checked < 2 * 24 * 3600 then + -- Check interval of 2 entire days + return + end + + core.settings:set("update_last_checked", tostring(time_now)) + + core.handle_async(function(params) + local http = core.get_http_api() + return http.fetch_sync(params) + end, { url = url }, function(result) + local json = result.succeeded and core.parse_json(result.data) + if type(json) ~= "table" or not json.latest then + core.log("error", "Failed to read JSON output from " .. url .. + ", status code = " .. result.code) + return + end + + on_version_info_received(json) + end) +end diff --git a/builtin/mainmenu/game_theme.lua b/builtin/mainmenu/game_theme.lua new file mode 100644 index 0000000..89e1b66 --- /dev/null +++ b/builtin/mainmenu/game_theme.lua @@ -0,0 +1,203 @@ +--Minetest +--Copyright (C) 2013 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +mm_game_theme = {} + +-------------------------------------------------------------------------------- +function mm_game_theme.init() + mm_game_theme.defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" .. + DIR_DELIM .. "pack" .. DIR_DELIM + mm_game_theme.basetexturedir = mm_game_theme.defaulttexturedir + + mm_game_theme.texturepack = core.settings:get("texture_path") + + mm_game_theme.gameid = nil + + mm_game_theme.music_handle = nil +end + +-------------------------------------------------------------------------------- +function mm_game_theme.update(tab,gamedetails) + if tab ~= "singleplayer" then + mm_game_theme.reset() + return + end + + if gamedetails == nil then + return + end + + mm_game_theme.update_game(gamedetails) +end + +-------------------------------------------------------------------------------- +function mm_game_theme.reset() + mm_game_theme.gameid = nil + local have_bg = false + local have_overlay = mm_game_theme.set_generic("overlay") + + if not have_overlay then + have_bg = mm_game_theme.set_generic("background") + end + + mm_game_theme.clear("header") + mm_game_theme.clear("footer") + core.set_clouds(false) + + mm_game_theme.set_generic("footer") + mm_game_theme.set_generic("header") + + if not have_bg then + if core.settings:get_bool("menu_clouds") then + core.set_clouds(true) + else + mm_game_theme.set_dirt_bg() + end + end + + if mm_game_theme.music_handle ~= nil then + core.sound_stop(mm_game_theme.music_handle) + end +end + +-------------------------------------------------------------------------------- +function mm_game_theme.update_game(gamedetails) + if mm_game_theme.gameid == gamedetails.id then + return + end + + local have_bg = false + local have_overlay = mm_game_theme.set_game("overlay",gamedetails) + + if not have_overlay then + have_bg = mm_game_theme.set_game("background",gamedetails) + end + + mm_game_theme.clear("header") + mm_game_theme.clear("footer") + core.set_clouds(false) + + if not have_bg then + + if core.settings:get_bool("menu_clouds") then + core.set_clouds(true) + else + mm_game_theme.set_dirt_bg() + end + end + + mm_game_theme.set_game("footer",gamedetails) + mm_game_theme.set_game("header",gamedetails) + + mm_game_theme.gameid = gamedetails.id +end + +-------------------------------------------------------------------------------- +function mm_game_theme.clear(identifier) + core.set_background(identifier,"") +end + +-------------------------------------------------------------------------------- +function mm_game_theme.set_generic(identifier) + --try texture pack first + if mm_game_theme.texturepack ~= nil then + local path = mm_game_theme.texturepack .. DIR_DELIM .."menu_" .. + identifier .. ".png" + if core.set_background(identifier,path) then + return true + end + end + + if mm_game_theme.defaulttexturedir ~= nil then + local path = mm_game_theme.defaulttexturedir .. DIR_DELIM .."menu_" .. + identifier .. ".png" + if core.set_background(identifier,path) then + return true + end + end + + return false +end + +-------------------------------------------------------------------------------- +function mm_game_theme.set_game(identifier, gamedetails) + + if gamedetails == nil then + return false + end + + mm_game_theme.set_music(gamedetails) + + if mm_game_theme.texturepack ~= nil then + local path = mm_game_theme.texturepack .. DIR_DELIM .. + gamedetails.id .. "_menu_" .. identifier .. ".png" + if core.set_background(identifier, path) then + return true + end + end + + -- Find out how many randomized textures the game provides + local n = 0 + local filename + local menu_files = core.get_dir_list(gamedetails.path .. DIR_DELIM .. "menu", false) + for i = 1, #menu_files do + filename = identifier .. "." .. i .. ".png" + if table.indexof(menu_files, filename) == -1 then + n = i - 1 + break + end + end + -- Select random texture, 0 means standard texture + n = math.random(0, n) + if n == 0 then + filename = identifier .. ".png" + else + filename = identifier .. "." .. n .. ".png" + end + + local path = gamedetails.path .. DIR_DELIM .. "menu" .. + DIR_DELIM .. filename + if core.set_background(identifier, path) then + return true + end + + return false +end + +-------------------------------------------------------------------------------- +function mm_game_theme.set_dirt_bg() + if mm_game_theme.texturepack ~= nil then + local path = mm_game_theme.texturepack .. DIR_DELIM .."default_dirt.png" + if core.set_background("background", path, true, 128) then + return true + end + end + + -- Use universal fallback texture in textures/base/pack + local minimalpath = defaulttexturedir .. "menu_bg.png" + core.set_background("background", minimalpath, true, 128) +end + +-------------------------------------------------------------------------------- +function mm_game_theme.set_music(gamedetails) + if mm_game_theme.music_handle ~= nil then + core.sound_stop(mm_game_theme.music_handle) + end + local music_path = gamedetails.path .. DIR_DELIM .. "menu" .. DIR_DELIM .. "theme" + mm_game_theme.music_handle = core.sound_play(music_path, true) +end diff --git a/builtin/mainmenu/generate_from_settingtypes.lua b/builtin/mainmenu/generate_from_settingtypes.lua new file mode 100644 index 0000000..0f551fb --- /dev/null +++ b/builtin/mainmenu/generate_from_settingtypes.lua @@ -0,0 +1,136 @@ +local settings = ... + +local concat = table.concat +local insert = table.insert +local sprintf = string.format +local rep = string.rep + +local minetest_example_header = [[ +# This file contains a list of all available settings and their default value for minetest.conf + +# By default, all the settings are commented and not functional. +# Uncomment settings by removing the preceding #. + +# minetest.conf is read by default from: +# ../minetest.conf +# ../../minetest.conf +# Any other path can be chosen by passing the path as a parameter +# to the program, eg. "minetest.exe --config ../minetest.conf.example". + +# Further documentation: +# http://wiki.minetest.net/ + +]] + +local group_format_template = [[ +# %s = { +# offset = %s, +# scale = %s, +# spread = (%s, %s, %s), +# seed = %s, +# octaves = %s, +# persistence = %s, +# lacunarity = %s, +# flags =%s +# } + +]] + +local function create_minetest_conf_example() + local result = { minetest_example_header } + for _, entry in ipairs(settings) do + if entry.type == "category" then + if entry.level == 0 then + insert(result, "#\n# " .. entry.name .. "\n#\n\n") + else + insert(result, rep("#", entry.level)) + insert(result, "# " .. entry.name .. "\n\n") + end + else + local group_format = false + if entry.noise_params and entry.values then + if entry.type == "noise_params_2d" or entry.type == "noise_params_3d" then + group_format = true + end + end + if entry.comment ~= "" then + for _, comment_line in ipairs(entry.comment:split("\n", true)) do + if comment_line == "" then + insert(result, "#\n") + else + insert(result, "# " .. comment_line .. "\n") + end + end + end + insert(result, "# type: " .. entry.type) + if entry.min then + insert(result, " min: " .. entry.min) + end + if entry.max then + insert(result, " max: " .. entry.max) + end + if entry.values and entry.noise_params == nil then + insert(result, " values: " .. concat(entry.values, ", ")) + end + if entry.possible then + insert(result, " possible values: " .. concat(entry.possible, ", ")) + end + insert(result, "\n") + if group_format == true then + local flags = entry.values[10] + if flags ~= "" then + flags = " "..flags + end + insert(result, sprintf(group_format_template, entry.name, entry.values[1], + entry.values[2], entry.values[3], entry.values[4], entry.values[5], + entry.values[6], entry.values[7], entry.values[8], entry.values[9], + flags)) + else + local append + if entry.default ~= "" then + append = " " .. entry.default + end + insert(result, sprintf("# %s =%s\n\n", entry.name, append or "")) + end + end + end + return concat(result) +end + +local translation_file_header = [[ +// This file is automatically generated +// It contains a bunch of fake gettext calls, to tell xgettext about the strings in config files +// To update it, refer to the bottom of builtin/mainmenu/dlg_settings_advanced.lua + +fake_function() {]] + +local function create_translation_file() + local result = { translation_file_header } + for _, entry in ipairs(settings) do + if entry.type == "category" then + insert(result, sprintf("\tgettext(%q);", entry.name)) + else + if entry.readable_name then + insert(result, sprintf("\tgettext(%q);", entry.readable_name)) + end + if entry.comment ~= "" then + local comment_escaped = entry.comment:gsub("\n", "\\n") + comment_escaped = comment_escaped:gsub("\"", "\\\"") + insert(result, "\tgettext(\"" .. comment_escaped .. "\");") + end + end + end + insert(result, "}\n") + return concat(result, "\n") +end + +local file = assert(io.open("minetest.conf.example", "w")) +file:write(create_minetest_conf_example()) +file:close() + +file = assert(io.open("src/settings_translation_file.cpp", "w")) +-- If 'minetest.conf.example' appears in the 'bin' folder, the line below may have to be +-- used instead. The file will also appear in the 'bin' folder. +--file = assert(io.open("settings_translation_file.cpp", "w")) +file:write(create_translation_file()) +file:close() diff --git a/builtin/mainmenu/init.lua b/builtin/mainmenu/init.lua new file mode 100644 index 0000000..c3a28a5 --- /dev/null +++ b/builtin/mainmenu/init.lua @@ -0,0 +1,131 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +mt_color_grey = "#AAAAAA" +mt_color_blue = "#6389FF" +mt_color_lightblue = "#99CCFF" +mt_color_green = "#72FF63" +mt_color_dark_green = "#25C191" +mt_color_orange = "#FF8800" +mt_color_red = "#FF3300" + +local menupath = core.get_mainmenu_path() +local basepath = core.get_builtin_path() +defaulttexturedir = core.get_texturepath_share() .. DIR_DELIM .. "base" .. + DIR_DELIM .. "pack" .. DIR_DELIM + +dofile(basepath .. "common" .. DIR_DELIM .. "filterlist.lua") +dofile(basepath .. "fstk" .. DIR_DELIM .. "buttonbar.lua") +dofile(basepath .. "fstk" .. DIR_DELIM .. "dialog.lua") +dofile(basepath .. "fstk" .. DIR_DELIM .. "tabview.lua") +dofile(basepath .. "fstk" .. DIR_DELIM .. "ui.lua") +dofile(menupath .. DIR_DELIM .. "async_event.lua") +dofile(menupath .. DIR_DELIM .. "common.lua") +dofile(menupath .. DIR_DELIM .. "pkgmgr.lua") +dofile(menupath .. DIR_DELIM .. "serverlistmgr.lua") +dofile(menupath .. DIR_DELIM .. "game_theme.lua") + +dofile(menupath .. DIR_DELIM .. "dlg_config_world.lua") +dofile(menupath .. DIR_DELIM .. "dlg_settings_advanced.lua") +dofile(menupath .. DIR_DELIM .. "dlg_contentstore.lua") +dofile(menupath .. DIR_DELIM .. "dlg_create_world.lua") +dofile(menupath .. DIR_DELIM .. "dlg_delete_content.lua") +dofile(menupath .. DIR_DELIM .. "dlg_delete_world.lua") +dofile(menupath .. DIR_DELIM .. "dlg_register.lua") +dofile(menupath .. DIR_DELIM .. "dlg_rename_modpack.lua") +dofile(menupath .. DIR_DELIM .. "dlg_version_info.lua") + +local tabs = {} + +tabs.settings = dofile(menupath .. DIR_DELIM .. "tab_settings.lua") +tabs.content = dofile(menupath .. DIR_DELIM .. "tab_content.lua") +tabs.about = dofile(menupath .. DIR_DELIM .. "tab_about.lua") +tabs.local_game = dofile(menupath .. DIR_DELIM .. "tab_local.lua") +tabs.play_online = dofile(menupath .. DIR_DELIM .. "tab_online.lua") + +-------------------------------------------------------------------------------- +local function main_event_handler(tabview, event) + if event == "MenuQuit" then + core.close() + end + return true +end + +-------------------------------------------------------------------------------- +local function init_globals() + -- Init gamedata + gamedata.worldindex = 0 + + menudata.worldlist = filterlist.create( + core.get_worlds, + compare_worlds, + -- Unique id comparison function + function(element, uid) + return element.name == uid + end, + -- Filter function + function(element, gameid) + return element.gameid == gameid + end + ) + + menudata.worldlist:add_sort_mechanism("alphabetic", sort_worlds_alphabetic) + menudata.worldlist:set_sortmode("alphabetic") + + if not core.settings:get("menu_last_game") then + local default_game = core.settings:get("default_game") or "minetest" + core.settings:set("menu_last_game", default_game) + end + + mm_game_theme.init() + + -- Create main tabview + local tv_main = tabview_create("maintab", {x = 12, y = 5.4}, {x = 0, y = 0}) + -- note: size would be 15.5,7.1 in real coordinates mode + + tv_main:set_autosave_tab(true) + tv_main:add(tabs.local_game) + tv_main:add(tabs.play_online) + + tv_main:add(tabs.content) + tv_main:add(tabs.settings) + tv_main:add(tabs.about) + + tv_main:set_global_event_handler(main_event_handler) + tv_main:set_fixed_size(false) + + local last_tab = core.settings:get("maintab_LAST") + if last_tab and tv_main.current_tab ~= last_tab then + tv_main:set_tab(last_tab) + end + + -- In case the folder of the last selected game has been deleted, + -- display "Minetest" as a header + if tv_main.current_tab == "local" then + local game = pkgmgr.find_by_gameid(core.settings:get("menu_last_game")) + if game == nil then + mm_game_theme.reset() + end + end + + ui.set_default("maintab") + check_new_version() + tv_main:show() + ui.update() +end + +init_globals() diff --git a/builtin/mainmenu/pkgmgr.lua b/builtin/mainmenu/pkgmgr.lua new file mode 100644 index 0000000..853509b --- /dev/null +++ b/builtin/mainmenu/pkgmgr.lua @@ -0,0 +1,929 @@ +--Minetest +--Copyright (C) 2013 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-------------------------------------------------------------------------------- +local function get_last_folder(text,count) + local parts = text:split(DIR_DELIM) + + if count == nil then + return parts[#parts] + end + + local retval = "" + for i=1,count,1 do + retval = retval .. parts[#parts - (count-i)] .. DIR_DELIM + end + + return retval +end + +local function cleanup_path(temppath) + + local parts = temppath:split("-") + temppath = "" + for i=1,#parts,1 do + if temppath ~= "" then + temppath = temppath .. "_" + end + temppath = temppath .. parts[i] + end + + parts = temppath:split(".") + temppath = "" + for i=1,#parts,1 do + if temppath ~= "" then + temppath = temppath .. "_" + end + temppath = temppath .. parts[i] + end + + parts = temppath:split("'") + temppath = "" + for i=1,#parts,1 do + if temppath ~= "" then + temppath = temppath .. "" + end + temppath = temppath .. parts[i] + end + + parts = temppath:split(" ") + temppath = "" + for i=1,#parts,1 do + if temppath ~= "" then + temppath = temppath + end + temppath = temppath .. parts[i] + end + + return temppath +end + +local function load_texture_packs(txtpath, retval) + local list = core.get_dir_list(txtpath, true) + local current_texture_path = core.settings:get("texture_path") + + for _, item in ipairs(list) do + if item ~= "base" then + local path = txtpath .. DIR_DELIM .. item .. DIR_DELIM + local conf = Settings(path .. "texture_pack.conf") + local enabled = path == current_texture_path + + local title = conf:get("title") or item + + -- list_* is only used if non-nil, else the regular versions are used. + retval[#retval + 1] = { + name = item, + title = title, + list_name = enabled and fgettext("$1 (Enabled)", item) or nil, + list_title = enabled and fgettext("$1 (Enabled)", title) or nil, + author = conf:get("author"), + release = tonumber(conf:get("release")) or 0, + type = "txp", + path = path, + enabled = enabled, + } + end + end +end + +function get_mods(path, virtual_path, retval, modpack) + local mods = core.get_dir_list(path, true) + + for _, name in ipairs(mods) do + if name:sub(1, 1) ~= "." then + local mod_path = path .. DIR_DELIM .. name + local mod_virtual_path = virtual_path .. "/" .. name + local toadd = { + dir_name = name, + parent_dir = path, + } + retval[#retval + 1] = toadd + + -- Get config file + local mod_conf + local modpack_conf = io.open(mod_path .. DIR_DELIM .. "modpack.conf") + if modpack_conf then + toadd.is_modpack = true + modpack_conf:close() + + mod_conf = Settings(mod_path .. DIR_DELIM .. "modpack.conf"):to_table() + if mod_conf.name then + name = mod_conf.name + toadd.is_name_explicit = true + end + else + mod_conf = Settings(mod_path .. DIR_DELIM .. "mod.conf"):to_table() + if mod_conf.name then + name = mod_conf.name + toadd.is_name_explicit = true + end + end + + -- Read from config + toadd.name = name + toadd.title = mod_conf.title + toadd.author = mod_conf.author + toadd.release = tonumber(mod_conf.release) or 0 + toadd.path = mod_path + toadd.virtual_path = mod_virtual_path + toadd.type = "mod" + + -- Check modpack.txt + -- Note: modpack.conf is already checked above + local modpackfile = io.open(mod_path .. DIR_DELIM .. "modpack.txt") + if modpackfile then + modpackfile:close() + toadd.is_modpack = true + end + + -- Deal with modpack contents + if modpack and modpack ~= "" then + toadd.modpack = modpack + elseif toadd.is_modpack then + toadd.type = "modpack" + toadd.is_modpack = true + get_mods(mod_path, mod_virtual_path, retval, name) + end + end + end +end + +--modmanager implementation +pkgmgr = {} + +function pkgmgr.get_texture_packs() + local txtpath = core.get_texturepath() + local txtpath_system = core.get_texturepath_share() + local retval = {} + + load_texture_packs(txtpath, retval) + -- on portable versions these two paths coincide. It avoids loading the path twice + if txtpath ~= txtpath_system then + load_texture_packs(txtpath_system, retval) + end + + table.sort(retval, function(a, b) + return a.name > b.name + end) + + return retval +end + +-------------------------------------------------------------------------------- +function pkgmgr.get_folder_type(path) + local testfile = io.open(path .. DIR_DELIM .. "init.lua","r") + if testfile ~= nil then + testfile:close() + return { type = "mod", path = path } + end + + testfile = io.open(path .. DIR_DELIM .. "modpack.conf","r") + if testfile ~= nil then + testfile:close() + return { type = "modpack", path = path } + end + + testfile = io.open(path .. DIR_DELIM .. "modpack.txt","r") + if testfile ~= nil then + testfile:close() + return { type = "modpack", path = path } + end + + testfile = io.open(path .. DIR_DELIM .. "game.conf","r") + if testfile ~= nil then + testfile:close() + return { type = "game", path = path } + end + + testfile = io.open(path .. DIR_DELIM .. "texture_pack.conf","r") + if testfile ~= nil then + testfile:close() + return { type = "txp", path = path } + end + + return nil +end + +------------------------------------------------------------------------------- +function pkgmgr.get_base_folder(temppath) + if temppath == nil then + return { type = "invalid", path = "" } + end + + local ret = pkgmgr.get_folder_type(temppath) + if ret then + return ret + end + + local subdirs = core.get_dir_list(temppath, true) + if #subdirs == 1 then + ret = pkgmgr.get_folder_type(temppath .. DIR_DELIM .. subdirs[1]) + if ret then + return ret + else + return { type = "invalid", path = temppath .. DIR_DELIM .. subdirs[1] } + end + end + + return nil +end + +-------------------------------------------------------------------------------- +function pkgmgr.isValidModname(modpath) + if modpath:find("-") ~= nil then + return false + end + + return true +end + +-------------------------------------------------------------------------------- +function pkgmgr.parse_register_line(line) + local pos1 = line:find("\"") + local pos2 = nil + if pos1 ~= nil then + pos2 = line:find("\"",pos1+1) + end + + if pos1 ~= nil and pos2 ~= nil then + local item = line:sub(pos1+1,pos2-1) + + if item ~= nil and + item ~= "" then + local pos3 = item:find(":") + + if pos3 ~= nil then + local retval = item:sub(1,pos3-1) + if retval ~= nil and + retval ~= "" then + return retval + end + end + end + end + return nil +end + +-------------------------------------------------------------------------------- +function pkgmgr.parse_dofile_line(modpath,line) + local pos1 = line:find("\"") + local pos2 = nil + if pos1 ~= nil then + pos2 = line:find("\"",pos1+1) + end + + if pos1 ~= nil and pos2 ~= nil then + local filename = line:sub(pos1+1,pos2-1) + + if filename ~= nil and + filename ~= "" and + filename:find(".lua") then + return pkgmgr.identify_modname(modpath,filename) + end + end + return nil +end + +-------------------------------------------------------------------------------- +function pkgmgr.identify_modname(modpath,filename) + local testfile = io.open(modpath .. DIR_DELIM .. filename,"r") + if testfile ~= nil then + local line = testfile:read() + + while line~= nil do + local modname = nil + + if line:find("minetest.register_tool") then + modname = pkgmgr.parse_register_line(line) + end + + if line:find("minetest.register_craftitem") then + modname = pkgmgr.parse_register_line(line) + end + + + if line:find("minetest.register_node") then + modname = pkgmgr.parse_register_line(line) + end + + if line:find("dofile") then + modname = pkgmgr.parse_dofile_line(modpath,line) + end + + if modname ~= nil then + testfile:close() + return modname + end + + line = testfile:read() + end + testfile:close() + end + + return nil +end +-------------------------------------------------------------------------------- +function pkgmgr.render_packagelist(render_list, use_technical_names, with_error) + if not render_list then + if not pkgmgr.global_mods then + pkgmgr.refresh_globals() + end + render_list = pkgmgr.global_mods + end + + local list = render_list:get_list() + local retval = {} + for i, v in ipairs(list) do + local color = "" + local icon = 0 + local error = with_error and with_error[v.virtual_path] + local function update_error(val) + if val and (not error or (error.type == "warning" and val.type == "error")) then + error = val + end + end + + if v.is_modpack then + local rawlist = render_list:get_raw_list() + color = mt_color_dark_green + + for j = 1, #rawlist do + if rawlist[j].modpack == list[i].name then + if with_error then + update_error(with_error[rawlist[j].virtual_path]) + end + + if rawlist[j].enabled then + icon = 1 + else + -- Modpack not entirely enabled so showing as grey + color = mt_color_grey + end + end + end + elseif v.is_game_content or v.type == "game" then + icon = 1 + color = mt_color_blue + + local rawlist = render_list:get_raw_list() + if v.type == "game" and with_error then + for j = 1, #rawlist do + if rawlist[j].is_game_content then + update_error(with_error[rawlist[j].virtual_path]) + end + end + end + elseif v.enabled or v.type == "txp" then + icon = 1 + color = mt_color_green + end + + if error then + if error.type == "warning" then + color = mt_color_orange + icon = 2 + else + color = mt_color_red + icon = 3 + end + end + + retval[#retval + 1] = color + if v.modpack ~= nil or v.loc == "game" then + retval[#retval + 1] = "1" + else + retval[#retval + 1] = "0" + end + + if with_error then + retval[#retval + 1] = icon + end + + if use_technical_names then + retval[#retval + 1] = core.formspec_escape(v.list_name or v.name) + else + retval[#retval + 1] = core.formspec_escape(v.list_title or v.list_name or v.title or v.name) + end + end + + return table.concat(retval, ",") +end + +-------------------------------------------------------------------------------- +function pkgmgr.get_dependencies(path) + if path == nil then + return {}, {} + end + + local info = core.get_content_info(path) + return info.depends or {}, info.optional_depends or {} +end + +----------- tests whether all of the mods in the modpack are enabled ----------- +function pkgmgr.is_modpack_entirely_enabled(data, name) + local rawlist = data.list:get_raw_list() + for j = 1, #rawlist do + if rawlist[j].modpack == name and not rawlist[j].enabled then + return false + end + end + return true +end + +local function disable_all_by_name(list, name, except) + for i=1, #list do + if list[i].name == name and list[i] ~= except then + list[i].enabled = false + end + end +end + +---------- toggles or en/disables a mod or modpack and its dependencies -------- +local function toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod) + if not mod.is_modpack then + -- Toggle or en/disable the mod + if toset == nil then + toset = not mod.enabled + end + if mod.enabled ~= toset then + toggled_mods[#toggled_mods+1] = mod.name + end + if toset then + -- Mark this mod for recursive dependency traversal + enabled_mods[mod.name] = true + + -- Disable other mods with the same name + disable_all_by_name(list, mod.name, mod) + end + mod.enabled = toset + else + -- Toggle or en/disable every mod in the modpack, + -- interleaved unsupported + for i = 1, #list do + if list[i].modpack == mod.name then + toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, list[i]) + end + end + end +end + +function pkgmgr.enable_mod(this, toset) + local list = this.data.list:get_list() + local mod = list[this.data.selected_mod] + + -- Game mods can't be enabled or disabled + if mod.is_game_content then + return + end + + local toggled_mods = {} + local enabled_mods = {} + toggle_mod_or_modpack(list, toggled_mods, enabled_mods, toset, mod) + + if next(enabled_mods) == nil then + -- Mod(s) were disabled, so no dependencies need to be enabled + table.sort(toggled_mods) + core.log("info", "Following mods were disabled: " .. + table.concat(toggled_mods, ", ")) + return + end + + -- Enable mods' depends after activation + + -- Make a list of mod ids indexed by their names. Among mods with the + -- same name, enabled mods take precedence, after which game mods take + -- precedence, being last in the mod list. + local mod_ids = {} + for id, mod2 in pairs(list) do + if mod2.type == "mod" and not mod2.is_modpack then + local prev_id = mod_ids[mod2.name] + if not prev_id or not list[prev_id].enabled then + mod_ids[mod2.name] = id + end + end + end + + -- to_enable is used as a DFS stack with sp as stack pointer + local to_enable = {} + local sp = 0 + for name in pairs(enabled_mods) do + local depends = pkgmgr.get_dependencies(list[mod_ids[name]].path) + for i = 1, #depends do + local dependency_name = depends[i] + if not enabled_mods[dependency_name] then + sp = sp+1 + to_enable[sp] = dependency_name + end + end + end + + -- If sp is 0, every dependency is already activated + while sp > 0 do + local name = to_enable[sp] + sp = sp-1 + + if not enabled_mods[name] then + enabled_mods[name] = true + local mod_to_enable = list[mod_ids[name]] + if not mod_to_enable then + core.log("warning", "Mod dependency \"" .. name .. + "\" not found!") + elseif not mod_to_enable.is_game_content then + if not mod_to_enable.enabled then + mod_to_enable.enabled = true + toggled_mods[#toggled_mods+1] = mod_to_enable.name + end + -- Push the dependencies of the dependency onto the stack + local depends = pkgmgr.get_dependencies(mod_to_enable.path) + for i = 1, #depends do + if not enabled_mods[depends[i]] then + sp = sp+1 + to_enable[sp] = depends[i] + end + end + end + end + end + + -- Log the list of enabled mods + table.sort(toggled_mods) + core.log("info", "Following mods were enabled: " .. + table.concat(toggled_mods, ", ")) +end + +-------------------------------------------------------------------------------- +function pkgmgr.get_worldconfig(worldpath) + local filename = worldpath .. + DIR_DELIM .. "world.mt" + + local worldfile = Settings(filename) + + local worldconfig = {} + worldconfig.global_mods = {} + worldconfig.game_mods = {} + + for key,value in pairs(worldfile:to_table()) do + if key == "gameid" then + worldconfig.id = value + elseif key:sub(0, 9) == "load_mod_" then + -- Compatibility: Check against "nil" which was erroneously used + -- as value for fresh configured worlds + worldconfig.global_mods[key] = value ~= "false" and value ~= "nil" + and value + else + worldconfig[key] = value + end + end + + --read gamemods + local gamespec = pkgmgr.find_by_gameid(worldconfig.id) + pkgmgr.get_game_mods(gamespec, worldconfig.game_mods) + + return worldconfig +end + +-------------------------------------------------------------------------------- +function pkgmgr.install_dir(type, path, basename, targetpath) + local basefolder = pkgmgr.get_base_folder(path) + + -- There's no good way to detect a texture pack, so let's just assume + -- it's correct for now. + if type == "txp" then + if basefolder and basefolder.type ~= "invalid" and basefolder.type ~= "txp" then + return nil, fgettext("Unable to install a $1 as a texture pack", basefolder.type) + end + + local from = basefolder and basefolder.path or path + if not targetpath then + targetpath = core.get_texturepath() .. DIR_DELIM .. basename + end + core.delete_dir(targetpath) + if not core.copy_dir(from, targetpath, false) then + return nil, + fgettext("Failed to install $1 to $2", basename, targetpath) + end + return targetpath, nil + + elseif not basefolder then + return nil, fgettext("Unable to find a valid mod or modpack") + end + + -- + -- Get destination + -- + if basefolder.type == "modpack" then + if type ~= "mod" then + return nil, fgettext("Unable to install a modpack as a $1", type) + end + + -- Get destination name for modpack + if targetpath then + core.delete_dir(targetpath) + else + local clean_path = nil + if basename ~= nil then + clean_path = basename + end + if not clean_path then + clean_path = get_last_folder(cleanup_path(basefolder.path)) + end + if clean_path then + targetpath = core.get_modpath() .. DIR_DELIM .. clean_path + else + return nil, + fgettext("Install Mod: Unable to find suitable folder name for modpack $1", + path) + end + end + elseif basefolder.type == "mod" then + if type ~= "mod" then + return nil, fgettext("Unable to install a mod as a $1", type) + end + + if targetpath then + core.delete_dir(targetpath) + else + local targetfolder = basename + if targetfolder == nil then + targetfolder = pkgmgr.identify_modname(basefolder.path, "init.lua") + end + + -- If heuristic failed try to use current foldername + if targetfolder == nil then + targetfolder = get_last_folder(basefolder.path) + end + + if targetfolder ~= nil and pkgmgr.isValidModname(targetfolder) then + targetpath = core.get_modpath() .. DIR_DELIM .. targetfolder + else + return nil, fgettext("Install Mod: Unable to find real mod name for: $1", path) + end + end + + elseif basefolder.type == "game" then + if type ~= "game" then + return nil, fgettext("Unable to install a game as a $1", type) + end + + if targetpath then + core.delete_dir(targetpath) + else + targetpath = core.get_gamepath() .. DIR_DELIM .. basename + end + else + error("basefolder didn't return a recognised type, this shouldn't happen") + end + + -- Copy it + core.delete_dir(targetpath) + if not core.copy_dir(basefolder.path, targetpath, false) then + return nil, + fgettext("Failed to install $1 to $2", basename, targetpath) + end + + if basefolder.type == "game" then + pkgmgr.update_gamelist() + else + pkgmgr.refresh_globals() + end + + return targetpath, nil +end + +-------------------------------------------------------------------------------- +function pkgmgr.preparemodlist(data) + local retval = {} + + local global_mods = {} + local game_mods = {} + + --read global mods + local modpaths = core.get_modpaths() + for key, modpath in pairs(modpaths) do + get_mods(modpath, key, global_mods) + end + + for i=1,#global_mods,1 do + global_mods[i].type = "mod" + global_mods[i].loc = "global" + global_mods[i].enabled = false + retval[#retval + 1] = global_mods[i] + end + + --read game mods + local gamespec = pkgmgr.find_by_gameid(data.gameid) + pkgmgr.get_game_mods(gamespec, game_mods) + + if #game_mods > 0 then + -- Add title + retval[#retval + 1] = { + type = "game", + is_game_content = true, + name = fgettext("$1 mods", gamespec.title), + path = gamespec.path + } + end + + for i=1,#game_mods,1 do + game_mods[i].type = "mod" + game_mods[i].loc = "game" + game_mods[i].is_game_content = true + retval[#retval + 1] = game_mods[i] + end + + if data.worldpath == nil then + return retval + end + + --read world mod configuration + local filename = data.worldpath .. + DIR_DELIM .. "world.mt" + + local worldfile = Settings(filename) + for key, value in pairs(worldfile:to_table()) do + if key:sub(1, 9) == "load_mod_" then + key = key:sub(10) + local mod_found = false + + local fallback_found = false + local fallback_mod = nil + + for i=1, #retval do + if retval[i].name == key and + not retval[i].is_modpack then + if core.is_yes(value) or retval[i].virtual_path == value then + retval[i].enabled = true + mod_found = true + break + elseif fallback_found then + -- Only allow fallback if only one mod matches + fallback_mod = nil + else + fallback_found = true + fallback_mod = retval[i] + end + end + end + + if not mod_found then + if fallback_mod and value:find("/") then + fallback_mod.enabled = true + else + core.log("info", "Mod: " .. key .. " " .. dump(value) .. " but not found") + end + end + end + end + + return retval +end + +function pkgmgr.compare_package(a, b) + return a and b and a.name == b.name and a.path == b.path +end + +-------------------------------------------------------------------------------- +function pkgmgr.comparemod(elem1,elem2) + if elem1 == nil or elem2 == nil then + return false + end + if elem1.name ~= elem2.name then + return false + end + if elem1.is_modpack ~= elem2.is_modpack then + return false + end + if elem1.type ~= elem2.type then + return false + end + if elem1.modpack ~= elem2.modpack then + return false + end + + if elem1.path ~= elem2.path then + return false + end + + return true +end + +-------------------------------------------------------------------------------- +function pkgmgr.mod_exists(basename) + + if pkgmgr.global_mods == nil then + pkgmgr.refresh_globals() + end + + if pkgmgr.global_mods:raw_index_by_uid(basename) > 0 then + return true + end + + return false +end + +-------------------------------------------------------------------------------- +function pkgmgr.get_global_mod(idx) + + if pkgmgr.global_mods == nil then + return nil + end + + if idx == nil or idx < 1 or + idx > pkgmgr.global_mods:size() then + return nil + end + + return pkgmgr.global_mods:get_list()[idx] +end + +-------------------------------------------------------------------------------- +function pkgmgr.refresh_globals() + local function is_equal(element,uid) --uid match + if element.name == uid then + return true + end + end + pkgmgr.global_mods = filterlist.create(pkgmgr.preparemodlist, + pkgmgr.comparemod, is_equal, nil, {}) + pkgmgr.global_mods:add_sort_mechanism("alphabetic", sort_mod_list) + pkgmgr.global_mods:set_sortmode("alphabetic") +end + +-------------------------------------------------------------------------------- +function pkgmgr.find_by_gameid(gameid) + for i=1,#pkgmgr.games,1 do + if pkgmgr.games[i].id == gameid then + return pkgmgr.games[i], i + end + end + return nil, nil +end + +-------------------------------------------------------------------------------- +function pkgmgr.get_game_mods(gamespec, retval) + if gamespec ~= nil and + gamespec.gamemods_path ~= nil and + gamespec.gamemods_path ~= "" then + get_mods(gamespec.gamemods_path, ("games/%s/mods"):format(gamespec.id), retval) + end +end + +-------------------------------------------------------------------------------- +function pkgmgr.get_game_modlist(gamespec) + local retval = "" + local game_mods = {} + pkgmgr.get_game_mods(gamespec, game_mods) + for i=1,#game_mods,1 do + if retval ~= "" then + retval = retval.."," + end + retval = retval .. game_mods[i].name + end + return retval +end + +-------------------------------------------------------------------------------- +function pkgmgr.get_game(index) + if index > 0 and index <= #pkgmgr.games then + return pkgmgr.games[index] + end + + return nil +end + +-------------------------------------------------------------------------------- +function pkgmgr.update_gamelist() + pkgmgr.games = core.get_games() +end + +-------------------------------------------------------------------------------- +function pkgmgr.gamelist() + local retval = "" + if #pkgmgr.games > 0 then + retval = retval .. core.formspec_escape(pkgmgr.games[1].title) + + for i=2,#pkgmgr.games,1 do + retval = retval .. "," .. core.formspec_escape(pkgmgr.games[i].title) + end + end + return retval +end + +-------------------------------------------------------------------------------- +-- read initial data +-------------------------------------------------------------------------------- +pkgmgr.update_gamelist() diff --git a/builtin/mainmenu/serverlistmgr.lua b/builtin/mainmenu/serverlistmgr.lua new file mode 100644 index 0000000..964d0c5 --- /dev/null +++ b/builtin/mainmenu/serverlistmgr.lua @@ -0,0 +1,252 @@ +--Minetest +--Copyright (C) 2020 rubenwardy +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +serverlistmgr = {} + +-------------------------------------------------------------------------------- +local function order_server_list(list) + local res = {} + --orders the favorite list after support + for i = 1, #list do + local fav = list[i] + if is_server_protocol_compat(fav.proto_min, fav.proto_max) then + res[#res + 1] = fav + end + end + for i = 1, #list do + local fav = list[i] + if not is_server_protocol_compat(fav.proto_min, fav.proto_max) then + res[#res + 1] = fav + end + end + return res +end + +local public_downloading = false + +-------------------------------------------------------------------------------- +function serverlistmgr.sync() + if not serverlistmgr.servers then + serverlistmgr.servers = {{ + name = fgettext("Loading..."), + description = fgettext_ne("Try reenabling public serverlist and check your internet connection.") + }} + end + + local serverlist_url = core.settings:get("serverlist_url") or "" + if not core.get_http_api or serverlist_url == "" then + serverlistmgr.servers = {{ + name = fgettext("Public server list is disabled"), + description = "" + }} + return + end + + if public_downloading then + return + end + public_downloading = true + + core.handle_async( + function(param) + local http = core.get_http_api() + local url = ("%s/list?proto_version_min=%d&proto_version_max=%d"):format( + core.settings:get("serverlist_url"), + core.get_min_supp_proto(), + core.get_max_supp_proto()) + + local response = http.fetch_sync({ url = url }) + if not response.succeeded then + return {} + end + + local retval = core.parse_json(response.data) + return retval and retval.list or {} + end, + nil, + function(result) + public_downloading = nil + local favs = order_server_list(result) + if favs[1] then + serverlistmgr.servers = favs + end + core.event_handler("Refresh") + end + ) +end + +-------------------------------------------------------------------------------- +local function get_favorites_path(folder) + local base = core.get_user_path() .. DIR_DELIM .. "client" .. DIR_DELIM .. "serverlist" .. DIR_DELIM + if folder then + return base + end + return base .. core.settings:get("serverlist_file") +end + +-------------------------------------------------------------------------------- +local function save_favorites(favorites) + local filename = core.settings:get("serverlist_file") + -- If setting specifies legacy format change the filename to the new one + if filename:sub(#filename - 3):lower() == ".txt" then + core.settings:set("serverlist_file", filename:sub(1, #filename - 4) .. ".json") + end + + assert(core.create_dir(get_favorites_path(true))) + core.safe_file_write(get_favorites_path(), core.write_json(favorites)) +end + +-------------------------------------------------------------------------------- +function serverlistmgr.read_legacy_favorites(path) + local file = io.open(path, "r") + if not file then + return nil + end + + local lines = {} + for line in file:lines() do + lines[#lines + 1] = line + end + file:close() + + local favorites = {} + + local i = 1 + while i < #lines do + local function pop() + local line = lines[i] + i = i + 1 + return line and line:trim() + end + + if pop():lower() == "[server]" then + local name = pop() + local address = pop() + local port = tonumber(pop()) + local description = pop() + + if name == "" then + name = nil + end + + if description == "" then + description = nil + end + + if not address or #address < 3 then + core.log("warning", "Malformed favorites file, missing address at line " .. i) + elseif not port or port < 1 or port > 65535 then + core.log("warning", "Malformed favorites file, missing port at line " .. i) + elseif (name and name:upper() == "[SERVER]") or + (address and address:upper() == "[SERVER]") or + (description and description:upper() == "[SERVER]") then + core.log("warning", "Potentially malformed favorites file, overran at line " .. i) + else + favorites[#favorites + 1] = { + name = name, + address = address, + port = port, + description = description + } + end + end + end + + return favorites +end + +-------------------------------------------------------------------------------- +local function read_favorites() + local path = get_favorites_path() + + -- If new format configured fall back to reading the legacy file + if path:sub(#path - 4):lower() == ".json" then + local file = io.open(path, "r") + if file then + local json = file:read("*all") + file:close() + return core.parse_json(json) + end + + path = path:sub(1, #path - 5) .. ".txt" + end + + local favs = serverlistmgr.read_legacy_favorites(path) + if favs then + save_favorites(favs) + os.remove(path) + end + return favs +end + +-------------------------------------------------------------------------------- +local function delete_favorite(favorites, del_favorite) + for i=1, #favorites do + local fav = favorites[i] + + if fav.address == del_favorite.address and fav.port == del_favorite.port then + table.remove(favorites, i) + return + end + end +end + +-------------------------------------------------------------------------------- +function serverlistmgr.get_favorites() + if serverlistmgr.favorites then + return serverlistmgr.favorites + end + + serverlistmgr.favorites = {} + + -- Add favorites, removing duplicates + local seen = {} + for _, fav in ipairs(read_favorites() or {}) do + local key = ("%s:%d"):format(fav.address:lower(), fav.port) + if not seen[key] then + seen[key] = true + serverlistmgr.favorites[#serverlistmgr.favorites + 1] = fav + end + end + + return serverlistmgr.favorites +end + +-------------------------------------------------------------------------------- +function serverlistmgr.add_favorite(new_favorite) + assert(type(new_favorite.port) == "number") + + -- Whitelist favorite keys + new_favorite = { + name = new_favorite.name, + address = new_favorite.address, + port = new_favorite.port, + description = new_favorite.description, + } + + local favorites = serverlistmgr.get_favorites() + delete_favorite(favorites, new_favorite) + table.insert(favorites, 1, new_favorite) + save_favorites(favorites) +end + +-------------------------------------------------------------------------------- +function serverlistmgr.delete_favorite(del_favorite) + local favorites = serverlistmgr.get_favorites() + delete_favorite(favorites, del_favorite) + save_favorites(favorites) +end diff --git a/builtin/mainmenu/tab_about.lua b/builtin/mainmenu/tab_about.lua new file mode 100644 index 0000000..a84ebce --- /dev/null +++ b/builtin/mainmenu/tab_about.lua @@ -0,0 +1,195 @@ +--Minetest +--Copyright (C) 2013 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-- https://github.com/orgs/minetest/teams/engine/members + +local core_developers = { + "Perttu Ahola (celeron55) [Project founder]", + "sfan5 ", + "ShadowNinja ", + "Nathanaëlle Courant (Nore/Ekdohibs) ", + "Loic Blot (nerzhul/nrz) ", + "Andrew Ward (rubenwardy) ", + "Krock/SmallJoker ", + "Lars Hofhansl ", + "v-rob ", + "hecks", + "Hugues Ross ", + "Dmitry Kostenko (x2048) ", +} + +local core_team = { + "Zughy [Issue triager]", +} + +-- For updating active/previous contributors, see the script in ./util/gather_git_credits.py + +local active_contributors = { + "Wuzzy [Features, translations, devtest]", + "Lars Müller [Lua optimizations and fixes]", + "Jude Melton-Houghton [Optimizations, bugfixes]", + "paradust7 [Performance, fixes, Irrlicht refactoring]", + "Desour [Fixes]", + "ROllerozxa [Main menu]", + "savilli [Bugfixes]", + "Lexi Hale [Particlespawner animation]", + "Liso [Shadow Mapping]", + "JosiahWI [Fixes, build system]", + "numzero [Graphics and rendering]", + "HybridDog [Fixes]", + "NeroBurner [Joystick]", + "pecksin [Clickable web links]", + "Daroc Alden [Fixes]", + "Jean-Patrick Guerrero (kilbith) [Fixes]", +} + +local previous_core_developers = { + "BlockMen", + "Maciej Kasatkin (RealBadAngel) [RIP]", + "Lisa Milne (darkrose) ", + "proller", + "Ilya Zhuravlev (xyz) ", + "PilzAdam ", + "est31 ", + "kahrl ", + "Ryan Kwolek (kwolekr) ", + "sapier", + "Zeno", + "Auke Kok (sofar) ", + "Aaron Suen ", + "paramat", + "Pierre-Yves Rollo ", +} + +local previous_contributors = { + "Nils Dagsson Moskopp (erlehmann) [Minetest logo]", + "red-001 ", + "Giuseppe Bilotta", + "ClobberXD", + "Dániel Juhász (juhdanad) ", + "MirceaKitsune ", + "MoNTE48", + "Constantin Wenger (SpeedProg)", + "Ciaran Gultnieks (CiaranG)", + "Paul Ouellette (pauloue)", + "stujones11", + "srifqi", + "Rogier ", + "Gregory Currie (gregorycu)", + "JacobF", + "Jeija ", +} + +local function prepare_credits(dest, source) + for _, s in ipairs(source) do + -- if there's text inside brackets make it gray-ish + s = s:gsub("%[.-%]", core.colorize("#aaa", "%1")) + dest[#dest+1] = s + end +end + +local function build_hacky_list(items, spacing) + spacing = spacing or 0.5 + local y = spacing / 2 + local ret = {} + for _, item in ipairs(items) do + if item ~= "" then + ret[#ret+1] = ("label[0,%f;%s]"):format(y, core.formspec_escape(item)) + end + y = y + spacing + end + return table.concat(ret, ""), y +end + +return { + name = "about", + caption = fgettext("About"), + cbf_formspec = function(tabview, name, tabdata) + local logofile = defaulttexturedir .. "logo.png" + local version = core.get_version() + + local credit_list = {} + table.insert_all(credit_list, { + core.colorize("#ff0", fgettext("Core Developers")) + }) + prepare_credits(credit_list, core_developers) + table.insert_all(credit_list, { + "", + core.colorize("#ff0", fgettext("Core Team")) + }) + prepare_credits(credit_list, core_team) + table.insert_all(credit_list, { + "", + core.colorize("#ff0", fgettext("Active Contributors")) + }) + prepare_credits(credit_list, active_contributors) + table.insert_all(credit_list, { + "", + core.colorize("#ff0", fgettext("Previous Core Developers")) + }) + prepare_credits(credit_list, previous_core_developers) + table.insert_all(credit_list, { + "", + core.colorize("#ff0", fgettext("Previous Contributors")) + }) + prepare_credits(credit_list, previous_contributors) + local credit_fs, scroll_height = build_hacky_list(credit_list) + -- account for the visible portion + scroll_height = math.max(0, scroll_height - 6.9) + + local fs = "image[1.5,0.6;2.5,2.5;" .. core.formspec_escape(logofile) .. "]" .. + "style[label_button;border=false]" .. + "button[0.1,3.4;5.3,0.5;label_button;" .. + core.formspec_escape(version.project .. " " .. version.string) .. "]" .. + "button[1.5,4.1;2.5,0.8;homepage;minetest.net]" .. + "scroll_container[5.5,0.1;9.5,6.9;scroll_credits;vertical;" .. + tostring(scroll_height / 1000) .. "]" .. credit_fs .. + "scroll_container_end[]".. + "scrollbar[15,0.1;0.4,6.9;vertical;scroll_credits;0]" + + -- Render information + fs = fs .. "style[label_button2;border=false]" .. + "button[0.1,6;5.3,1;label_button2;" .. + fgettext("Active renderer:") .. "\n" .. + core.formspec_escape(core.get_screen_info().render_info) .. "]" + + if PLATFORM == "Android" then + fs = fs .. "button[0.5,5.1;4.5,0.8;share_debug;" .. fgettext("Share debug log") .. "]" + else + fs = fs .. "tooltip[userdata;" .. + fgettext("Opens the directory that contains user-provided worlds, games, mods,\n" .. + "and texture packs in a file manager / explorer.") .. "]" + fs = fs .. "button[0.5,5.1;4.5,0.8;userdata;" .. fgettext("Open User Data Directory") .. "]" + end + + return fs, "size[15.5,7.1,false]real_coordinates[true]" + end, + cbf_button_handler = function(this, fields, name, tabdata) + if fields.homepage then + core.open_url("https://www.minetest.net") + end + + if fields.share_debug then + local path = core.get_user_path() .. DIR_DELIM .. "debug.txt" + core.share_file(path) + end + + if fields.userdata then + core.open_dir(core.get_user_path()) + end + end, +} diff --git a/builtin/mainmenu/tab_content.lua b/builtin/mainmenu/tab_content.lua new file mode 100644 index 0000000..5e14d19 --- /dev/null +++ b/builtin/mainmenu/tab_content.lua @@ -0,0 +1,237 @@ +--Minetest +--Copyright (C) 2014 sapier +--Copyright (C) 2018 rubenwardy +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local packages_raw +local packages + +-------------------------------------------------------------------------------- +local function get_formspec(tabview, name, tabdata) + if pkgmgr.global_mods == nil then + pkgmgr.refresh_globals() + end + if pkgmgr.games == nil then + pkgmgr.update_gamelist() + end + + if packages == nil then + packages_raw = {} + table.insert_all(packages_raw, pkgmgr.games) + table.insert_all(packages_raw, pkgmgr.get_texture_packs()) + table.insert_all(packages_raw, pkgmgr.global_mods:get_list()) + + local function get_data() + return packages_raw + end + + local function is_equal(element, uid) --uid match + return (element.type == "game" and element.id == uid) or + element.name == uid + end + + packages = filterlist.create(get_data, pkgmgr.compare_package, + is_equal, nil, {}) + end + + if tabdata.selected_pkg == nil then + tabdata.selected_pkg = 1 + end + + local use_technical_names = core.settings:get_bool("show_technical_names") + + + local retval = + "label[0.05,-0.25;".. fgettext("Installed Packages:") .. "]" .. + "tablecolumns[color;tree;text]" .. + "table[0,0.25;5.1,4.3;pkglist;" .. + pkgmgr.render_packagelist(packages, use_technical_names) .. + ";" .. tabdata.selected_pkg .. "]" .. + "button[0,4.85;5.25,0.5;btn_contentdb;".. fgettext("Browse online content") .. "]" + + + local selected_pkg + if filterlist.size(packages) >= tabdata.selected_pkg then + selected_pkg = packages:get_list()[tabdata.selected_pkg] + end + + if selected_pkg ~= nil then + --check for screenshot beeing available + local screenshotfilename = selected_pkg.path .. DIR_DELIM .. "screenshot.png" + local screenshotfile, error = io.open(screenshotfilename, "r") + + local modscreenshot + if error == nil then + screenshotfile:close() + modscreenshot = screenshotfilename + end + + if modscreenshot == nil then + modscreenshot = defaulttexturedir .. "no_screenshot.png" + end + + local info = core.get_content_info(selected_pkg.path) + local desc = fgettext("No package description available") + if info.description and info.description:trim() ~= "" then + desc = info.description + end + + local title_and_name + if selected_pkg.type == "game" then + title_and_name = selected_pkg.name + else + title_and_name = (selected_pkg.title or selected_pkg.name) .. "\n" .. + core.colorize("#BFBFBF", selected_pkg.name) + end + + retval = retval .. + "image[5.5,0;3,2;" .. core.formspec_escape(modscreenshot) .. "]" .. + "label[8.25,0.6;" .. core.formspec_escape(title_and_name) .. "]" .. + "box[5.5,2.2;6.15,2.35;#000]" + + if selected_pkg.type == "mod" then + if selected_pkg.is_modpack then + retval = retval .. + "button[8.65,4.65;3.25,1;btn_mod_mgr_rename_modpack;" .. + fgettext("Rename") .. "]" + else + --show dependencies + desc = desc .. "\n\n" + local toadd_hard = table.concat(info.depends or {}, "\n") + local toadd_soft = table.concat(info.optional_depends or {}, "\n") + if toadd_hard == "" and toadd_soft == "" then + desc = desc .. fgettext("No dependencies.") + else + if toadd_hard ~= "" then + desc = desc ..fgettext("Dependencies:") .. + "\n" .. toadd_hard + end + if toadd_soft ~= "" then + if toadd_hard ~= "" then + desc = desc .. "\n\n" + end + desc = desc .. fgettext("Optional dependencies:") .. + "\n" .. toadd_soft + end + end + end + + else + if selected_pkg.type == "txp" then + if selected_pkg.enabled then + retval = retval .. + "button[8.65,4.65;3.25,1;btn_mod_mgr_disable_txp;" .. + fgettext("Disable Texture Pack") .. "]" + else + retval = retval .. + "button[8.65,4.65;3.25,1;btn_mod_mgr_use_txp;" .. + fgettext("Use Texture Pack") .. "]" + end + end + end + + retval = retval .. "textarea[5.85,2.2;6.35,2.9;;" .. + fgettext("Information:") .. ";" .. desc .. "]" + + if core.may_modify_path(selected_pkg.path) then + retval = retval .. + "button[5.5,4.65;3.25,1;btn_mod_mgr_delete_mod;" .. + fgettext("Uninstall Package") .. "]" + end + end + return retval +end + +-------------------------------------------------------------------------------- +local function handle_doubleclick(pkg) + if pkg.type == "txp" then + if core.settings:get("texture_path") == pkg.path then + core.settings:set("texture_path", "") + else + core.settings:set("texture_path", pkg.path) + end + packages = nil + + mm_game_theme.init() + mm_game_theme.reset() + end +end + +-------------------------------------------------------------------------------- +local function handle_buttons(tabview, fields, tabname, tabdata) + if fields["pkglist"] ~= nil then + local event = core.explode_table_event(fields["pkglist"]) + tabdata.selected_pkg = event.row + if event.type == "DCL" then + handle_doubleclick(packages:get_list()[tabdata.selected_pkg]) + end + return true + end + + if fields["btn_contentdb"] ~= nil then + local dlg = create_store_dlg() + dlg:set_parent(tabview) + tabview:hide() + dlg:show() + packages = nil + return true + end + + if fields["btn_mod_mgr_rename_modpack"] ~= nil then + local mod = packages:get_list()[tabdata.selected_pkg] + local dlg_renamemp = create_rename_modpack_dlg(mod) + dlg_renamemp:set_parent(tabview) + tabview:hide() + dlg_renamemp:show() + packages = nil + return true + end + + if fields["btn_mod_mgr_delete_mod"] ~= nil then + local mod = packages:get_list()[tabdata.selected_pkg] + local dlg_delmod = create_delete_content_dlg(mod) + dlg_delmod:set_parent(tabview) + tabview:hide() + dlg_delmod:show() + packages = nil + return true + end + + if fields.btn_mod_mgr_use_txp or fields.btn_mod_mgr_disable_txp then + local txp_path = "" + if fields.btn_mod_mgr_use_txp then + txp_path = packages:get_list()[tabdata.selected_pkg].path + end + + core.settings:set("texture_path", txp_path) + packages = nil + + mm_game_theme.init() + mm_game_theme.reset() + return true + end + + return false +end + +-------------------------------------------------------------------------------- +return { + name = "content", + caption = fgettext("Content"), + cbf_formspec = get_formspec, + cbf_button_handler = handle_buttons, + on_change = pkgmgr.update_gamelist +} diff --git a/builtin/mainmenu/tab_local.lua b/builtin/mainmenu/tab_local.lua new file mode 100644 index 0000000..f8de10d --- /dev/null +++ b/builtin/mainmenu/tab_local.lua @@ -0,0 +1,393 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +local current_game, singleplayer_refresh_gamebar +local valid_disabled_settings = { + ["enable_damage"]=true, + ["creative_mode"]=true, + ["enable_server"]=true, +} + +-- Currently chosen game in gamebar for theming and filtering +function current_game() + local last_game_id = core.settings:get("menu_last_game") + local game = pkgmgr.find_by_gameid(last_game_id) + + return game +end + +-- Apply menu changes from given game +function apply_game(game) + core.set_topleft_text(game.name) + core.settings:set("menu_last_game", game.id) + menudata.worldlist:set_filtercriteria(game.id) + + mm_game_theme.update("singleplayer", game) -- this refreshes the formspec + + local index = filterlist.get_current_index(menudata.worldlist, + tonumber(core.settings:get("mainmenu_last_selected_world"))) + if not index or index < 1 then + local selected = core.get_textlist_index("sp_worlds") + if selected ~= nil and selected < #menudata.worldlist:get_list() then + index = selected + else + index = #menudata.worldlist:get_list() + end + end + menu_worldmt_legacy(index) +end + +function singleplayer_refresh_gamebar() + + local old_bar = ui.find_by_name("game_button_bar") + if old_bar ~= nil then + old_bar:delete() + end + + local function game_buttonbar_button_handler(fields) + if fields.game_open_cdb then + local maintab = ui.find_by_name("maintab") + local dlg = create_store_dlg("game") + dlg:set_parent(maintab) + maintab:hide() + dlg:show() + return true + end + + for _, game in ipairs(pkgmgr.games) do + if fields["game_btnbar_" .. game.id] then + apply_game(game) + return true + end + end + end + + local btnbar = buttonbar_create("game_button_bar", + game_buttonbar_button_handler, + {x=-0.3,y=5.9}, "horizontal", {x=12.4,y=1.15}) + + for _, game in ipairs(pkgmgr.games) do + local btn_name = "game_btnbar_" .. game.id + + local image = nil + local text = nil + local tooltip = core.formspec_escape(game.title) + + if (game.menuicon_path or "") ~= "" then + image = core.formspec_escape(game.menuicon_path) + else + local part1 = game.id:sub(1,5) + local part2 = game.id:sub(6,10) + local part3 = game.id:sub(11) + + text = part1 .. "\n" .. part2 + if part3 ~= "" then + text = text .. "\n" .. part3 + end + end + btnbar:add_button(btn_name, text, image, tooltip) + end + + local plus_image = core.formspec_escape(defaulttexturedir .. "plus.png") + btnbar:add_button("game_open_cdb", "", plus_image, fgettext("Install games from ContentDB")) +end + +local function get_disabled_settings(game) + if not game then + return {} + end + + local gameconfig = Settings(game.path .. "/game.conf") + local disabled_settings = {} + if gameconfig then + local disabled_settings_str = (gameconfig:get("disabled_settings") or ""):split() + for _, value in pairs(disabled_settings_str) do + local state = false + value = value:trim() + if string.sub(value, 1, 1) == "!" then + state = true + value = string.sub(value, 2) + end + if valid_disabled_settings[value] then + disabled_settings[value] = state + else + core.log("error", "Invalid disabled setting in game.conf: "..tostring(value)) + end + end + end + return disabled_settings +end + +local function get_formspec(tabview, name, tabdata) + local retval = "" + + local index = filterlist.get_current_index(menudata.worldlist, + tonumber(core.settings:get("mainmenu_last_selected_world"))) + local list = menudata.worldlist:get_list() + local world = list and index and list[index] + local game + if world then + game = pkgmgr.find_by_gameid(world.gameid) + else + game = current_game() + end + local disabled_settings = get_disabled_settings(game) + + local creative, damage, host = "", "", "" + + -- Y offsets for game settings checkboxes + local y = -0.2 + local yo = 0.45 + + if disabled_settings["creative_mode"] == nil then + creative = "checkbox[0,"..y..";cb_creative_mode;".. fgettext("Creative Mode") .. ";" .. + dump(core.settings:get_bool("creative_mode")) .. "]" + y = y + yo + end + if disabled_settings["enable_damage"] == nil then + damage = "checkbox[0,"..y..";cb_enable_damage;".. fgettext("Enable Damage") .. ";" .. + dump(core.settings:get_bool("enable_damage")) .. "]" + y = y + yo + end + if disabled_settings["enable_server"] == nil then + host = "checkbox[0,"..y..";cb_server;".. fgettext("Host Server") ..";" .. + dump(core.settings:get_bool("enable_server")) .. "]" + y = y + yo + end + + retval = retval .. + "button[3.9,3.8;2.8,1;world_delete;".. fgettext("Delete") .. "]" .. + "button[6.55,3.8;2.8,1;world_configure;".. fgettext("Select Mods") .. "]" .. + "button[9.2,3.8;2.8,1;world_create;".. fgettext("New") .. "]" .. + "label[3.9,-0.05;".. fgettext("Select World:") .. "]".. + creative .. + damage .. + host .. + "textlist[3.9,0.4;7.9,3.45;sp_worlds;" .. + menu_render_worldlist() .. + ";" .. index .. "]" + + if core.settings:get_bool("enable_server") and disabled_settings["enable_server"] == nil then + retval = retval .. + "button[7.9,4.75;4.1,1;play;".. fgettext("Host Game") .. "]" .. + "checkbox[0,"..y..";cb_server_announce;" .. fgettext("Announce Server") .. ";" .. + dump(core.settings:get_bool("server_announce")) .. "]" .. + "field[0.3,2.85;3.8,0.5;te_playername;" .. fgettext("Name") .. ";" .. + core.formspec_escape(core.settings:get("name")) .. "]" .. + "pwdfield[0.3,4.05;3.8,0.5;te_passwd;" .. fgettext("Password") .. "]" + + local bind_addr = core.settings:get("bind_address") + if bind_addr ~= nil and bind_addr ~= "" then + retval = retval .. + "field[0.3,5.25;2.5,0.5;te_serveraddr;" .. fgettext("Bind Address") .. ";" .. + core.formspec_escape(core.settings:get("bind_address")) .. "]" .. + "field[2.85,5.25;1.25,0.5;te_serverport;" .. fgettext("Port") .. ";" .. + core.formspec_escape(core.settings:get("port")) .. "]" + else + retval = retval .. + "field[0.3,5.25;3.8,0.5;te_serverport;" .. fgettext("Server Port") .. ";" .. + core.formspec_escape(core.settings:get("port")) .. "]" + end + else + retval = retval .. + "button[7.9,4.75;4.1,1;play;" .. fgettext("Play Game") .. "]" + end + + return retval +end + +local function main_button_handler(this, fields, name, tabdata) + + assert(name == "local") + + local world_doubleclick = false + + if fields["sp_worlds"] ~= nil then + local event = core.explode_textlist_event(fields["sp_worlds"]) + local selected = core.get_textlist_index("sp_worlds") + + menu_worldmt_legacy(selected) + + if event.type == "DCL" then + world_doubleclick = true + end + + if event.type == "CHG" and selected ~= nil then + core.settings:set("mainmenu_last_selected_world", + menudata.worldlist:get_raw_index(selected)) + return true + end + end + + if menu_handle_key_up_down(fields,"sp_worlds","mainmenu_last_selected_world") then + return true + end + + if fields["cb_creative_mode"] then + core.settings:set("creative_mode", fields["cb_creative_mode"]) + local selected = core.get_textlist_index("sp_worlds") + menu_worldmt(selected, "creative_mode", fields["cb_creative_mode"]) + + return true + end + + if fields["cb_enable_damage"] then + core.settings:set("enable_damage", fields["cb_enable_damage"]) + local selected = core.get_textlist_index("sp_worlds") + menu_worldmt(selected, "enable_damage", fields["cb_enable_damage"]) + + return true + end + + if fields["cb_server"] then + core.settings:set("enable_server", fields["cb_server"]) + + return true + end + + if fields["cb_server_announce"] then + core.settings:set("server_announce", fields["cb_server_announce"]) + local selected = core.get_textlist_index("srv_worlds") + menu_worldmt(selected, "server_announce", fields["cb_server_announce"]) + + return true + end + + if fields["play"] ~= nil or world_doubleclick or fields["key_enter"] then + local selected = core.get_textlist_index("sp_worlds") + gamedata.selected_world = menudata.worldlist:get_raw_index(selected) + + if selected == nil or gamedata.selected_world == 0 then + gamedata.errormessage = + fgettext("No world created or selected!") + return true + end + + -- Update last game + local world = menudata.worldlist:get_raw_element(gamedata.selected_world) + local game_obj + if world then + game_obj = pkgmgr.find_by_gameid(world.gameid) + core.settings:set("menu_last_game", game_obj.id) + end + + local disabled_settings = get_disabled_settings(game_obj) + for k, _ in pairs(valid_disabled_settings) do + local v = disabled_settings[k] + if v ~= nil then + if k == "enable_server" and v == true then + error("Setting 'enable_server' cannot be force-enabled! The game.conf needs to be fixed.") + end + core.settings:set_bool(k, disabled_settings[k]) + end + end + + if core.settings:get_bool("enable_server") then + gamedata.playername = fields["te_playername"] + gamedata.password = fields["te_passwd"] + gamedata.port = fields["te_serverport"] + gamedata.address = "" + + core.settings:set("port",gamedata.port) + if fields["te_serveraddr"] ~= nil then + core.settings:set("bind_address",fields["te_serveraddr"]) + end + else + gamedata.singleplayer = true + end + + core.start() + return true + end + + if fields["world_create"] ~= nil then + local create_world_dlg = create_create_world_dlg() + create_world_dlg:set_parent(this) + this:hide() + create_world_dlg:show() + mm_game_theme.update("singleplayer", current_game()) + return true + end + + if fields["world_delete"] ~= nil then + local selected = core.get_textlist_index("sp_worlds") + if selected ~= nil and + selected <= menudata.worldlist:size() then + local world = menudata.worldlist:get_list()[selected] + if world ~= nil and + world.name ~= nil and + world.name ~= "" then + local index = menudata.worldlist:get_raw_index(selected) + local delete_world_dlg = create_delete_world_dlg(world.name,index) + delete_world_dlg:set_parent(this) + this:hide() + delete_world_dlg:show() + mm_game_theme.update("singleplayer",current_game()) + end + end + + return true + end + + if fields["world_configure"] ~= nil then + local selected = core.get_textlist_index("sp_worlds") + if selected ~= nil then + local configdialog = + create_configure_world_dlg( + menudata.worldlist:get_raw_index(selected)) + + if (configdialog ~= nil) then + configdialog:set_parent(this) + this:hide() + configdialog:show() + mm_game_theme.update("singleplayer",current_game()) + end + end + + return true + end +end + +local function on_change(type, old_tab, new_tab) + if (type == "ENTER") then + local game = current_game() + if game then + apply_game(game) + end + + singleplayer_refresh_gamebar() + ui.find_by_name("game_button_bar"):show() + else + menudata.worldlist:set_filtercriteria(nil) + local gamebar = ui.find_by_name("game_button_bar") + if gamebar then + gamebar:hide() + end + core.set_topleft_text("") + mm_game_theme.update(new_tab,nil) + end +end + +-------------------------------------------------------------------------------- +return { + name = "local", + caption = fgettext("Start Game"), + cbf_formspec = get_formspec, + cbf_button_handler = main_button_handler, + on_change = on_change +} diff --git a/builtin/mainmenu/tab_online.lua b/builtin/mainmenu/tab_online.lua new file mode 100644 index 0000000..ad5f79e --- /dev/null +++ b/builtin/mainmenu/tab_online.lua @@ -0,0 +1,427 @@ +--Minetest +--Copyright (C) 2014 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local function get_sorted_servers() + local servers = { + fav = {}, + public = {}, + incompatible = {} + } + + local favs = serverlistmgr.get_favorites() + local taken_favs = {} + local result = menudata.search_result or serverlistmgr.servers + for _, server in ipairs(result) do + server.is_favorite = false + for index, fav in ipairs(favs) do + if server.address == fav.address and server.port == fav.port then + taken_favs[index] = true + server.is_favorite = true + break + end + end + server.is_compatible = is_server_protocol_compat(server.proto_min, server.proto_max) + if server.is_favorite then + table.insert(servers.fav, server) + elseif server.is_compatible then + table.insert(servers.public, server) + else + table.insert(servers.incompatible, server) + end + end + + if not menudata.search_result then + for index, fav in ipairs(favs) do + if not taken_favs[index] then + table.insert(servers.fav, fav) + end + end + end + + return servers +end + +local function get_formspec(tabview, name, tabdata) + -- Update the cached supported proto info, + -- it may have changed after a change by the settings menu. + common_update_cached_supp_proto() + + if not tabdata.search_for then + tabdata.search_for = "" + end + + local retval = + -- Search + "field[0.25,0.25;7,0.75;te_search;;" .. core.formspec_escape(tabdata.search_for) .. "]" .. + "container[7.25,0.25]" .. + "image_button[0,0;0.75,0.75;" .. core.formspec_escape(defaulttexturedir .. "search.png") .. ";btn_mp_search;]" .. + "image_button[0.75,0;0.75,0.75;" .. core.formspec_escape(defaulttexturedir .. "clear.png") .. ";btn_mp_clear;]" .. + "image_button[1.5,0;0.75,0.75;" .. core.formspec_escape(defaulttexturedir .. "refresh.png") .. ";btn_mp_refresh;]" .. + "tooltip[btn_mp_clear;" .. fgettext("Clear") .. "]" .. + "tooltip[btn_mp_search;" .. fgettext("Search") .. "]" .. + "tooltip[btn_mp_refresh;" .. fgettext("Refresh") .. "]" .. + "container_end[]" .. + + "container[9.75,0]" .. + "box[0,0;5.75,7;#666666]" .. + + -- Address / Port + "label[0.25,0.35;" .. fgettext("Address") .. "]" .. + "label[4.25,0.35;" .. fgettext("Port") .. "]" .. + "field[0.25,0.5;4,0.75;te_address;;" .. + core.formspec_escape(core.settings:get("address")) .. "]" .. + "field[4.25,0.5;1.25,0.75;te_port;;" .. + core.formspec_escape(core.settings:get("remote_port")) .. "]" .. + + -- Description Background + "label[0.25,1.6;" .. fgettext("Server Description") .. "]" .. + "box[0.25,1.85;5.25,2.7;#999999]".. + + -- Name / Password + "container[0,4.8]" .. + "label[0.25,0;" .. fgettext("Name") .. "]" .. + "label[2.875,0;" .. fgettext("Password") .. "]" .. + "field[0.25,0.2;2.625,0.75;te_name;;" .. core.formspec_escape(core.settings:get("name")) .. "]" .. + "pwdfield[2.875,0.2;2.625,0.75;te_pwd;]" .. + "container_end[]" .. + + -- Connect + "button[3,6;2.5,0.75;btn_mp_login;" .. fgettext("Login") .. "]" + + if core.settings:get_bool("enable_split_login_register") then + retval = retval .. "button[0.25,6;2.5,0.75;btn_mp_register;" .. fgettext("Register") .. "]" + end + + if tabdata.selected then + if gamedata.fav then + retval = retval .. "tooltip[btn_delete_favorite;" .. fgettext("Remove favorite") .. "]" + retval = retval .. "style[btn_delete_favorite;padding=6]" + retval = retval .. "image_button[5,1.3;0.5,0.5;" .. core.formspec_escape(defaulttexturedir .. + "server_favorite_delete.png") .. ";btn_delete_favorite;]" + end + if gamedata.serverdescription then + retval = retval .. "textarea[0.25,1.85;5.2,2.75;;;" .. + core.formspec_escape(gamedata.serverdescription) .. "]" + end + end + + retval = retval .. "container_end[]" + + -- Table + retval = retval .. "tablecolumns[" .. + "image,tooltip=" .. fgettext("Ping") .. "," .. + "0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," .. + "1=" .. core.formspec_escape(defaulttexturedir .. "server_ping_4.png") .. "," .. + "2=" .. core.formspec_escape(defaulttexturedir .. "server_ping_3.png") .. "," .. + "3=" .. core.formspec_escape(defaulttexturedir .. "server_ping_2.png") .. "," .. + "4=" .. core.formspec_escape(defaulttexturedir .. "server_ping_1.png") .. "," .. + "5=" .. core.formspec_escape(defaulttexturedir .. "server_favorite.png") .. "," .. + "6=" .. core.formspec_escape(defaulttexturedir .. "server_public.png") .. "," .. + "7=" .. core.formspec_escape(defaulttexturedir .. "server_incompatible.png") .. ";" .. + "color,span=1;" .. + "text,align=inline;".. + "color,span=1;" .. + "text,align=inline,width=4.25;" .. + "image,tooltip=" .. fgettext("Creative mode") .. "," .. + "0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," .. + "1=" .. core.formspec_escape(defaulttexturedir .. "server_flags_creative.png") .. "," .. + "align=inline,padding=0.25,width=1.5;" .. + --~ PvP = Player versus Player + "image,tooltip=" .. fgettext("Damage / PvP") .. "," .. + "0=" .. core.formspec_escape(defaulttexturedir .. "blank.png") .. "," .. + "1=" .. core.formspec_escape(defaulttexturedir .. "server_flags_damage.png") .. "," .. + "2=" .. core.formspec_escape(defaulttexturedir .. "server_flags_pvp.png") .. "," .. + "align=inline,padding=0.25,width=1.5;" .. + "color,align=inline,span=1;" .. + "text,align=inline,padding=1]" .. + "table[0.25,1;9.25,5.75;servers;" + + local servers = get_sorted_servers() + + local dividers = { + fav = "5,#ffff00," .. fgettext("Favorites") .. ",,,0,0,,", + public = "6,#4bdd42," .. fgettext("Public Servers") .. ",,,0,0,,", + incompatible = "7,"..mt_color_grey.."," .. fgettext("Incompatible Servers") .. ",,,0,0,," + } + local order = {"fav", "public", "incompatible"} + + tabdata.lookup = {} -- maps row number to server + local rows = {} + for _, section in ipairs(order) do + local section_servers = servers[section] + if next(section_servers) ~= nil then + rows[#rows + 1] = dividers[section] + for _, server in ipairs(section_servers) do + tabdata.lookup[#rows + 1] = server + rows[#rows + 1] = render_serverlist_row(server) + end + end + end + + retval = retval .. table.concat(rows, ",") + + if tabdata.selected then + retval = retval .. ";" .. tabdata.selected .. "]" + else + retval = retval .. ";0]" + end + + return retval, "size[15.5,7,false]real_coordinates[true]" +end + +-------------------------------------------------------------------------------- + +local function search_server_list(input) + menudata.search_result = nil + if #serverlistmgr.servers < 2 then + return + end + + -- setup the keyword list + local keywords = {} + for word in input:gmatch("%S+") do + word = word:gsub("(%W)", "%%%1") + table.insert(keywords, word) + end + + if #keywords == 0 then + return + end + + menudata.search_result = {} + + -- Search the serverlist + local search_result = {} + for i = 1, #serverlistmgr.servers do + local server = serverlistmgr.servers[i] + local found = 0 + for k = 1, #keywords do + local keyword = keywords[k] + if server.name then + local sername = server.name:lower() + local _, count = sername:gsub(keyword, keyword) + found = found + count * 4 + end + + if server.description then + local desc = server.description:lower() + local _, count = desc:gsub(keyword, keyword) + found = found + count * 2 + end + end + if found > 0 then + local points = (#serverlistmgr.servers - i) / 5 + found + server.points = points + table.insert(search_result, server) + end + end + + if #search_result == 0 then + return + end + + table.sort(search_result, function(a, b) + return a.points > b.points + end) + menudata.search_result = search_result +end + +local function set_selected_server(tabdata, idx, server) + -- reset selection + if idx == nil or server == nil then + tabdata.selected = nil + + core.settings:set("address", "") + core.settings:set("remote_port", "30000") + return + end + + local address = server.address + local port = server.port + gamedata.serverdescription = server.description + + gamedata.fav = false + for _, fav in ipairs(serverlistmgr.get_favorites()) do + if address == fav.address and port == fav.port then + gamedata.fav = true + break + end + end + + if address and port then + core.settings:set("address", address) + core.settings:set("remote_port", port) + end + tabdata.selected = idx +end + +local function main_button_handler(tabview, fields, name, tabdata) + if fields.te_name then + gamedata.playername = fields.te_name + core.settings:set("name", fields.te_name) + end + + if fields.servers then + local event = core.explode_table_event(fields.servers) + local server = tabdata.lookup[event.row] + + if server then + if event.type == "DCL" then + if not is_server_protocol_compat_or_error( + server.proto_min, server.proto_max) then + return true + end + + gamedata.address = server.address + gamedata.port = server.port + gamedata.playername = fields.te_name + gamedata.selected_world = 0 + + if fields.te_pwd then + gamedata.password = fields.te_pwd + end + + gamedata.servername = server.name + gamedata.serverdescription = server.description + + if gamedata.address and gamedata.port then + core.settings:set("address", gamedata.address) + core.settings:set("remote_port", gamedata.port) + core.start() + end + return true + end + if event.type == "CHG" then + set_selected_server(tabdata, event.row, server) + return true + end + end + end + + if fields.btn_delete_favorite then + local idx = core.get_table_index("servers") + if not idx then return end + local server = tabdata.lookup[idx] + if not server then return end + + serverlistmgr.delete_favorite(server) + -- the server at [idx+1] will be at idx once list is refreshed + set_selected_server(tabdata, idx, tabdata.lookup[idx+1]) + return true + end + + if fields.btn_mp_clear then + tabdata.search_for = "" + menudata.search_result = nil + return true + end + + if fields.btn_mp_search or fields.key_enter_field == "te_search" then + tabdata.search_for = fields.te_search + search_server_list(fields.te_search:lower()) + if menudata.search_result then + -- first server in row 2 due to header + set_selected_server(tabdata, 2, menudata.search_result[1]) + end + + return true + end + + if fields.btn_mp_refresh then + serverlistmgr.sync() + return true + end + + if (fields.btn_mp_login or fields.key_enter) + and fields.te_address ~= "" and fields.te_port then + gamedata.playername = fields.te_name + gamedata.password = fields.te_pwd + gamedata.address = fields.te_address + gamedata.port = tonumber(fields.te_port) + + local enable_split_login_register = core.settings:get_bool("enable_split_login_register") + gamedata.allow_login_or_register = enable_split_login_register and "login" or "any" + gamedata.selected_world = 0 + + local idx = core.get_table_index("servers") + local server = idx and tabdata.lookup[idx] + + set_selected_server(tabdata) + + if server and server.address == gamedata.address and + server.port == gamedata.port then + + serverlistmgr.add_favorite(server) + + gamedata.servername = server.name + gamedata.serverdescription = server.description + + if not is_server_protocol_compat_or_error( + server.proto_min, server.proto_max) then + return true + end + else + gamedata.servername = "" + gamedata.serverdescription = "" + + serverlistmgr.add_favorite({ + address = gamedata.address, + port = gamedata.port, + }) + end + + core.settings:set("address", gamedata.address) + core.settings:set("remote_port", gamedata.port) + + core.start() + return true + end + + if fields.btn_mp_register and fields.te_address ~= "" and fields.te_port then + local idx = core.get_table_index("servers") + local server = idx and tabdata.lookup[idx] + if server and (server.address ~= fields.te_address or server.port ~= tonumber(fields.te_port)) then + server = nil + end + + if server and not is_server_protocol_compat_or_error( + server.proto_min, server.proto_max) then + return true + end + + local dlg = create_register_dialog(fields.te_address, tonumber(fields.te_port), server) + dlg:set_parent(tabview) + tabview:hide() + dlg:show() + return true + end + + return false +end + +local function on_change(type, old_tab, new_tab) + if type == "LEAVE" then return end + serverlistmgr.sync() +end + +return { + name = "online", + caption = fgettext("Join Game"), + cbf_formspec = get_formspec, + cbf_button_handler = main_button_handler, + on_change = on_change +} diff --git a/builtin/mainmenu/tab_settings.lua b/builtin/mainmenu/tab_settings.lua new file mode 100644 index 0000000..21c77aa --- /dev/null +++ b/builtin/mainmenu/tab_settings.lua @@ -0,0 +1,403 @@ +--Minetest +--Copyright (C) 2013 sapier +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +-------------------------------------------------------------------------------- + +local labels = { + leaves = { + fgettext("Opaque Leaves"), + fgettext("Simple Leaves"), + fgettext("Fancy Leaves") + }, + node_highlighting = { + fgettext("Node Outlining"), + fgettext("Node Highlighting"), + fgettext("None") + }, + filters = { + fgettext("No Filter"), + fgettext("Bilinear Filter"), + fgettext("Trilinear Filter") + }, + mipmap = { + fgettext("No Mipmap"), + fgettext("Mipmap"), + fgettext("Mipmap + Aniso. Filter") + }, + antialiasing = { + fgettext("None"), + fgettext("2x"), + fgettext("4x"), + fgettext("8x") + }, + shadow_levels = { + fgettext("Disabled"), + fgettext("Very Low"), + fgettext("Low"), + fgettext("Medium"), + fgettext("High"), + fgettext("Very High") + } +} + +local dd_options = { + leaves = { + table.concat(labels.leaves, ","), + {"opaque", "simple", "fancy"} + }, + node_highlighting = { + table.concat(labels.node_highlighting, ","), + {"box", "halo", "none"} + }, + filters = { + table.concat(labels.filters, ","), + {"", "bilinear_filter", "trilinear_filter"} + }, + mipmap = { + table.concat(labels.mipmap, ","), + {"", "mip_map", "anisotropic_filter"} + }, + antialiasing = { + table.concat(labels.antialiasing, ","), + {"0", "2", "4", "8"} + }, + shadow_levels = { + table.concat(labels.shadow_levels, ","), + { "0", "1", "2", "3", "4", "5" } + } +} + +local getSettingIndex = { + Leaves = function() + local style = core.settings:get("leaves_style") + for idx, name in pairs(dd_options.leaves[2]) do + if style == name then return idx end + end + return 1 + end, + NodeHighlighting = function() + local style = core.settings:get("node_highlighting") + for idx, name in pairs(dd_options.node_highlighting[2]) do + if style == name then return idx end + end + return 1 + end, + Filter = function() + if core.settings:get(dd_options.filters[2][3]) == "true" then + return 3 + elseif core.settings:get(dd_options.filters[2][3]) == "false" and + core.settings:get(dd_options.filters[2][2]) == "true" then + return 2 + end + return 1 + end, + Mipmap = function() + if core.settings:get(dd_options.mipmap[2][3]) == "true" then + return 3 + elseif core.settings:get(dd_options.mipmap[2][3]) == "false" and + core.settings:get(dd_options.mipmap[2][2]) == "true" then + return 2 + end + return 1 + end, + Antialiasing = function() + local antialiasing_setting = core.settings:get("fsaa") + for i = 1, #dd_options.antialiasing[2] do + if antialiasing_setting == dd_options.antialiasing[2][i] then + return i + end + end + return 1 + end, + ShadowMapping = function() + local shadow_setting = core.settings:get("shadow_levels") + for i = 1, #dd_options.shadow_levels[2] do + if shadow_setting == dd_options.shadow_levels[2][i] then + return i + end + end + return 1 + end +} + +local function antialiasing_fname_to_name(fname) + for i = 1, #labels.antialiasing do + if fname == labels.antialiasing[i] then + return dd_options.antialiasing[2][i] + end + end + return 0 +end + +local function formspec(tabview, name, tabdata) + local tab_string = + "box[0,0;3.75,4.5;#999999]" .. + "checkbox[0.25,0;cb_smooth_lighting;" .. fgettext("Smooth Lighting") .. ";" + .. dump(core.settings:get_bool("smooth_lighting")) .. "]" .. + "checkbox[0.25,0.5;cb_particles;" .. fgettext("Particles") .. ";" + .. dump(core.settings:get_bool("enable_particles")) .. "]" .. + "checkbox[0.25,1;cb_3d_clouds;" .. fgettext("3D Clouds") .. ";" + .. dump(core.settings:get_bool("enable_3d_clouds")) .. "]" .. + "checkbox[0.25,1.5;cb_opaque_water;" .. fgettext("Opaque Water") .. ";" + .. dump(core.settings:get_bool("opaque_water")) .. "]" .. + "checkbox[0.25,2.0;cb_connected_glass;" .. fgettext("Connected Glass") .. ";" + .. dump(core.settings:get_bool("connected_glass")) .. "]" .. + "dropdown[0.25,2.8;3.5;dd_node_highlighting;" .. dd_options.node_highlighting[1] .. ";" + .. getSettingIndex.NodeHighlighting() .. "]" .. + "dropdown[0.25,3.6;3.5;dd_leaves_style;" .. dd_options.leaves[1] .. ";" + .. getSettingIndex.Leaves() .. "]" .. + "box[4,0;3.75,4.9;#999999]" .. + "label[4.25,0.1;" .. fgettext("Texturing:") .. "]" .. + "dropdown[4.25,0.55;3.5;dd_filters;" .. dd_options.filters[1] .. ";" + .. getSettingIndex.Filter() .. "]" .. + "dropdown[4.25,1.35;3.5;dd_mipmap;" .. dd_options.mipmap[1] .. ";" + .. getSettingIndex.Mipmap() .. "]" .. + "label[4.25,2.15;" .. fgettext("Antialiasing:") .. "]" .. + "dropdown[4.25,2.6;3.5;dd_antialiasing;" .. dd_options.antialiasing[1] .. ";" + .. getSettingIndex.Antialiasing() .. "]" .. + "box[8,0;3.75,4.5;#999999]" + + local video_driver = core.settings:get("video_driver") + local shaders_enabled = core.settings:get_bool("enable_shaders") + if video_driver == "opengl" then + tab_string = tab_string .. + "checkbox[8.25,0;cb_shaders;" .. fgettext("Shaders") .. ";" + .. tostring(shaders_enabled) .. "]" + elseif video_driver == "ogles2" then + tab_string = tab_string .. + "checkbox[8.25,0;cb_shaders;" .. fgettext("Shaders (experimental)") .. ";" + .. tostring(shaders_enabled) .. "]" + else + core.settings:set_bool("enable_shaders", false) + shaders_enabled = false + tab_string = tab_string .. + "label[8.38,0.2;" .. core.colorize("#888888", + fgettext("Shaders (unavailable)")) .. "]" + end + + tab_string = tab_string .. + "button[8,4.75;3.95,1;btn_change_keys;" + .. fgettext("Change Keys") .. "]" + + tab_string = tab_string .. + "button[0,4.75;3.95,1;btn_advanced_settings;" + .. fgettext("All Settings") .. "]" + + + if core.settings:get("touchscreen_threshold") ~= nil then + tab_string = tab_string .. + "label[4.25,3.5;" .. fgettext("Touch threshold (px):") .. "]" .. + "dropdown[4.25,3.95;3.5;dd_touchthreshold;0,10,20,30,40,50;" .. + ((tonumber(core.settings:get("touchscreen_threshold")) / 10) + 1) .. + "]" + else + tab_string = tab_string .. + "label[4.25,3.65;" .. fgettext("Screen:") .. "]" .. + "checkbox[4.25,3.9;cb_autosave_screensize;" .. fgettext("Autosave Screen Size") .. ";" + .. dump(core.settings:get_bool("autosave_screensize")) .. "]" + end + + if shaders_enabled then + tab_string = tab_string .. + "checkbox[8.25,0.5;cb_tonemapping;" .. fgettext("Tone Mapping") .. ";" + .. dump(core.settings:get_bool("tone_mapping")) .. "]" .. + "checkbox[8.25,1;cb_waving_water;" .. fgettext("Waving Liquids") .. ";" + .. dump(core.settings:get_bool("enable_waving_water")) .. "]" .. + "checkbox[8.25,1.5;cb_waving_leaves;" .. fgettext("Waving Leaves") .. ";" + .. dump(core.settings:get_bool("enable_waving_leaves")) .. "]" .. + "checkbox[8.25,2;cb_waving_plants;" .. fgettext("Waving Plants") .. ";" + .. dump(core.settings:get_bool("enable_waving_plants")) .. "]" + + if video_driver == "opengl" then + tab_string = tab_string .. + "label[8.25,2.8;" .. fgettext("Dynamic shadows:") .. "]" .. + "label[8.25,3.2;" .. fgettext("(game support required)") .. "]" .. + "dropdown[8.25,3.7;3.5;dd_shadows;" .. dd_options.shadow_levels[1] .. ";" + .. getSettingIndex.ShadowMapping() .. "]" + else + tab_string = tab_string .. + "label[8.38,2.7;" .. core.colorize("#888888", + fgettext("Dynamic shadows")) .. "]" + end + else + tab_string = tab_string .. + "label[8.38,0.7;" .. core.colorize("#888888", + fgettext("Tone Mapping")) .. "]" .. + "label[8.38,1.2;" .. core.colorize("#888888", + fgettext("Waving Liquids")) .. "]" .. + "label[8.38,1.7;" .. core.colorize("#888888", + fgettext("Waving Leaves")) .. "]" .. + "label[8.38,2.2;" .. core.colorize("#888888", + fgettext("Waving Plants")) .. "]".. + "label[8.38,2.7;" .. core.colorize("#888888", + fgettext("Dynamic shadows")) .. "]" + end + + return tab_string +end + +-------------------------------------------------------------------------------- +local function handle_settings_buttons(this, fields, tabname, tabdata) + + if fields["btn_advanced_settings"] ~= nil then + local adv_settings_dlg = create_adv_settings_dlg() + adv_settings_dlg:set_parent(this) + this:hide() + adv_settings_dlg:show() + --mm_game_theme.update("singleplayer", current_game()) + return true + end + if fields["cb_smooth_lighting"] then + core.settings:set("smooth_lighting", fields["cb_smooth_lighting"]) + return true + end + if fields["cb_particles"] then + core.settings:set("enable_particles", fields["cb_particles"]) + return true + end + if fields["cb_3d_clouds"] then + core.settings:set("enable_3d_clouds", fields["cb_3d_clouds"]) + return true + end + if fields["cb_opaque_water"] then + core.settings:set("opaque_water", fields["cb_opaque_water"]) + return true + end + if fields["cb_connected_glass"] then + core.settings:set("connected_glass", fields["cb_connected_glass"]) + return true + end + if fields["cb_autosave_screensize"] then + core.settings:set("autosave_screensize", fields["cb_autosave_screensize"]) + return true + end + if fields["cb_shaders"] then + core.settings:set("enable_shaders", fields["cb_shaders"]) + return true + end + if fields["cb_tonemapping"] then + core.settings:set("tone_mapping", fields["cb_tonemapping"]) + return true + end + if fields["cb_waving_water"] then + core.settings:set("enable_waving_water", fields["cb_waving_water"]) + return true + end + if fields["cb_waving_leaves"] then + core.settings:set("enable_waving_leaves", fields["cb_waving_leaves"]) + end + if fields["cb_waving_plants"] then + core.settings:set("enable_waving_plants", fields["cb_waving_plants"]) + return true + end + if fields["btn_change_keys"] then + core.show_keys_menu() + return true + end + if fields["cb_touchscreen_target"] then + core.settings:set("touchtarget", fields["cb_touchscreen_target"]) + return true + end + + --Note dropdowns have to be handled LAST! + local ddhandled = false + + for i = 1, #labels.leaves do + if fields["dd_leaves_style"] == labels.leaves[i] then + core.settings:set("leaves_style", dd_options.leaves[2][i]) + ddhandled = true + end + end + for i = 1, #labels.node_highlighting do + if fields["dd_node_highlighting"] == labels.node_highlighting[i] then + core.settings:set("node_highlighting", dd_options.node_highlighting[2][i]) + ddhandled = true + end + end + if fields["dd_filters"] == labels.filters[1] then + core.settings:set("bilinear_filter", "false") + core.settings:set("trilinear_filter", "false") + ddhandled = true + elseif fields["dd_filters"] == labels.filters[2] then + core.settings:set("bilinear_filter", "true") + core.settings:set("trilinear_filter", "false") + ddhandled = true + elseif fields["dd_filters"] == labels.filters[3] then + core.settings:set("bilinear_filter", "false") + core.settings:set("trilinear_filter", "true") + ddhandled = true + end + if fields["dd_mipmap"] == labels.mipmap[1] then + core.settings:set("mip_map", "false") + core.settings:set("anisotropic_filter", "false") + ddhandled = true + elseif fields["dd_mipmap"] == labels.mipmap[2] then + core.settings:set("mip_map", "true") + core.settings:set("anisotropic_filter", "false") + ddhandled = true + elseif fields["dd_mipmap"] == labels.mipmap[3] then + core.settings:set("mip_map", "true") + core.settings:set("anisotropic_filter", "true") + ddhandled = true + end + if fields["dd_antialiasing"] then + core.settings:set("fsaa", + antialiasing_fname_to_name(fields["dd_antialiasing"])) + ddhandled = true + end + if fields["dd_touchthreshold"] then + core.settings:set("touchscreen_threshold", fields["dd_touchthreshold"]) + ddhandled = true + end + + for i = 1, #labels.shadow_levels do + if fields["dd_shadows"] == labels.shadow_levels[i] then + core.settings:set("shadow_levels", dd_options.shadow_levels[2][i]) + ddhandled = true + end + end + + if fields["dd_shadows"] == labels.shadow_levels[1] then + core.settings:set("enable_dynamic_shadows", "false") + else + local shadow_presets = { + [2] = { 62, 512, "true", 0, "false" }, + [3] = { 93, 1024, "true", 0, "false" }, + [4] = { 140, 2048, "true", 1, "false" }, + [5] = { 210, 4096, "true", 2, "true" }, + [6] = { 300, 8192, "true", 2, "true" }, + } + local s = shadow_presets[table.indexof(labels.shadow_levels, fields["dd_shadows"])] + if s then + core.settings:set("enable_dynamic_shadows", "true") + core.settings:set("shadow_map_max_distance", s[1]) + core.settings:set("shadow_map_texture_size", s[2]) + core.settings:set("shadow_map_texture_32bit", s[3]) + core.settings:set("shadow_filters", s[4]) + core.settings:set("shadow_map_color", s[5]) + end + end + + return ddhandled +end + +return { + name = "settings", + caption = fgettext("Settings"), + cbf_formspec = formspec, + cbf_button_handler = handle_settings_buttons +} diff --git a/builtin/mainmenu/tests/favorites_wellformed.txt b/builtin/mainmenu/tests/favorites_wellformed.txt new file mode 100644 index 0000000..8b87b43 --- /dev/null +++ b/builtin/mainmenu/tests/favorites_wellformed.txt @@ -0,0 +1,29 @@ +[server] + +127.0.0.1 +30000 + + +[server] + +localhost +30000 + + +[server] + +vps.rubenwardy.com +30001 + + +[server] + +gundul.ddnss.de +39155 + + +[server] +VanessaE's Dreambuilder creative Server +daconcepts.com +30000 +VanessaE's Dreambuilder creative-mode server. Lots of mods, whitelisted buckets. diff --git a/builtin/mainmenu/tests/serverlistmgr_spec.lua b/builtin/mainmenu/tests/serverlistmgr_spec.lua new file mode 100644 index 0000000..ab7a6c6 --- /dev/null +++ b/builtin/mainmenu/tests/serverlistmgr_spec.lua @@ -0,0 +1,38 @@ +_G.core = {} +_G.vector = {metatable = {}} +_G.unpack = table.unpack +_G.serverlistmgr = {} + +dofile("builtin/common/vector.lua") +dofile("builtin/common/misc_helpers.lua") +dofile("builtin/mainmenu/serverlistmgr.lua") + +local base = "builtin/mainmenu/tests/" + +describe("legacy favorites", function() + it("loads well-formed correctly", function() + local favs = serverlistmgr.read_legacy_favorites(base .. "favorites_wellformed.txt") + + local expected = { + { + address = "127.0.0.1", + port = 30000, + }, + + { address = "localhost", port = 30000 }, + + { address = "vps.rubenwardy.com", port = 30001 }, + + { address = "gundul.ddnss.de", port = 39155 }, + + { + address = "daconcepts.com", + port = 30000, + name = "VanessaE's Dreambuilder creative Server", + description = "VanessaE's Dreambuilder creative-mode server. Lots of mods, whitelisted buckets." + }, + } + + assert.same(expected, favs) + end) +end) diff --git a/builtin/profiler/init.lua b/builtin/profiler/init.lua new file mode 100644 index 0000000..7f63dfa --- /dev/null +++ b/builtin/profiler/init.lua @@ -0,0 +1,80 @@ +--Minetest +--Copyright (C) 2016 T4im +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local S = core.get_translator("__builtin") + +local function get_bool_default(name, default) + local val = core.settings:get_bool(name) + if val == nil then + return default + end + return val +end + +local profiler_path = core.get_builtin_path().."profiler"..DIR_DELIM +local profiler = {} +local sampler = assert(loadfile(profiler_path .. "sampling.lua"))(profiler) +local instrumentation = assert(loadfile(profiler_path .. "instrumentation.lua"))(profiler, sampler, get_bool_default) +local reporter = dofile(profiler_path .. "reporter.lua") +profiler.instrument = instrumentation.instrument + +--- +-- Delayed registration of the /profiler chat command +-- Is called later, after `core.register_chatcommand` was set up. +-- +function profiler.init_chatcommand() + local instrument_profiler = get_bool_default("instrument.profiler", false) + if instrument_profiler then + instrumentation.init_chatcommand() + end + + local param_usage = S("print [] | dump [] | save [ []] | reset") + core.register_chatcommand("profiler", { + description = S("Handle the profiler and profiling data"), + params = param_usage, + privs = { server=true }, + func = function(name, param) + local command, arg0 = string.match(param, "([^ ]+) ?(.*)") + local args = arg0 and string.split(arg0, " ") + + if command == "dump" then + core.log("action", reporter.print(sampler.profile, arg0)) + return true, S("Statistics written to action log.") + elseif command == "print" then + return true, reporter.print(sampler.profile, arg0) + elseif command == "save" then + return reporter.save(sampler.profile, args[1] or "txt", args[2]) + elseif command == "reset" then + sampler.reset() + return true, S("Statistics were reset.") + end + + return false, + S("Usage: @1", param_usage) .. "\n" .. + S("Format can be one of txt, csv, lua, json, json_pretty (structures may be subject to change).") + end + }) + + if not instrument_profiler then + instrumentation.init_chatcommand() + end +end + +sampler.init() +instrumentation.init() + +return profiler diff --git a/builtin/profiler/instrumentation.lua b/builtin/profiler/instrumentation.lua new file mode 100644 index 0000000..f80314b --- /dev/null +++ b/builtin/profiler/instrumentation.lua @@ -0,0 +1,235 @@ +--Minetest +--Copyright (C) 2016 T4im +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local format, pairs, type = string.format, pairs, type +local core, get_current_modname = core, core.get_current_modname +local profiler, sampler, get_bool_default = ... + +local instrument_builtin = get_bool_default("instrument.builtin", false) + +local register_functions = { + register_globalstep = 0, + register_playerevent = 0, + register_on_placenode = 0, + register_on_dignode = 0, + register_on_punchnode = 0, + register_on_generated = 0, + register_on_newplayer = 0, + register_on_dieplayer = 0, + register_on_respawnplayer = 0, + register_on_prejoinplayer = 0, + register_on_joinplayer = 0, + register_on_leaveplayer = 0, + register_on_cheat = 0, + register_on_chat_message = 0, + register_on_player_receive_fields = 0, + register_on_craft = 0, + register_craft_predict = 0, + register_on_protection_violation = 0, + register_on_item_eat = 0, + register_on_punchplayer = 0, + register_on_player_hpchange = 0, +} + +--- +-- Create an unique instrument name. +-- Generate a missing label with a running index number. +-- +local counts = {} +local function generate_name(def) + local class, label, func_name = def.class, def.label, def.func_name + if label then + if class or func_name then + return format("%s '%s' %s", class or "", label, func_name or ""):trim() + end + return format("%s", label):trim() + elseif label == false then + return format("%s", class or func_name):trim() + end + + local index_id = def.mod .. (class or func_name) + local index = counts[index_id] or 1 + counts[index_id] = index + 1 + return format("%s[%d] %s", class or func_name, index, class and func_name or ""):trim() +end + +--- +-- Keep `measure` and the closure in `instrument` lean, as these, and their +-- directly called functions are the overhead that is caused by instrumentation. +-- +local time, log = core.get_us_time, sampler.log +local function measure(modname, instrument_name, start, ...) + log(modname, instrument_name, time() - start) + return ... +end +--- Automatically instrument a function to measure and log to the sampler. +-- def = { +-- mod = "", +-- class = "", +-- func_name = "", +-- -- if nil, will create a label based on registration order +-- label = "" | false, +-- } +local function instrument(def) + if not def or not def.func then + return + end + def.mod = def.mod or get_current_modname() or "??" + local modname = def.mod + local instrument_name = generate_name(def) + local func = def.func + + if not instrument_builtin and modname == "*builtin*" then + return func + end + + return function(...) + -- This tail-call allows passing all return values of `func` + -- also called https://en.wikipedia.org/wiki/Continuation_passing_style + -- Compared to table creation and unpacking it won't lose `nil` returns + -- and is expected to be faster + -- `measure` will be executed after func(...) + local start = time() + return measure(modname, instrument_name, start, func(...)) + end +end + +local function can_be_called(func) + -- It has to be a function or callable table + return type(func) == "function" or + ((type(func) == "table" or type(func) == "userdata") and + getmetatable(func) and getmetatable(func).__call) +end + +local function assert_can_be_called(func, func_name, level) + if not can_be_called(func) then + -- Then throw an *helpful* error, by pointing on our caller instead of us. + error(format("Invalid argument to %s. Expected function-like type instead of '%s'.", + func_name, type(func)), level + 1) + end +end + +--- +-- Wraps a registration function `func` in such a way, +-- that it will automatically instrument any callback function passed as first argument. +-- +local function instrument_register(func, func_name) + local register_name = func_name:gsub("^register_", "", 1) + return function(callback, ...) + assert_can_be_called(callback, func_name, 2) + register_functions[func_name] = register_functions[func_name] + 1 + return func(instrument { + func = callback, + func_name = register_name + }, ...) + end +end + +local function init_chatcommand() + if get_bool_default("instrument.chatcommand", true) then + local orig_register_chatcommand = core.register_chatcommand + core.register_chatcommand = function(cmd, def) + def.func = instrument { + func = def.func, + label = "/" .. cmd, + } + orig_register_chatcommand(cmd, def) + end + end +end + +--- +-- Start instrumenting selected functions +-- +local function init() + if get_bool_default("instrument.entity", true) then + -- Explicitly declare entity api-methods. + -- Simple iteration would ignore lookup via __index. + local entity_instrumentation = { + "on_activate", + "on_deactivate", + "on_step", + "on_punch", + "on_rightclick", + "get_staticdata", + } + -- Wrap register_entity() to instrument them on registration. + local orig_register_entity = core.register_entity + core.register_entity = function(name, prototype) + local modname = get_current_modname() + for _, func_name in pairs(entity_instrumentation) do + prototype[func_name] = instrument { + func = prototype[func_name], + mod = modname, + func_name = func_name, + label = prototype.label, + } + end + orig_register_entity(name,prototype) + end + end + + if get_bool_default("instrument.abm", true) then + -- Wrap register_abm() to automatically instrument abms. + local orig_register_abm = core.register_abm + core.register_abm = function(spec) + spec.action = instrument { + func = spec.action, + class = "ABM", + label = spec.label, + } + orig_register_abm(spec) + end + end + + if get_bool_default("instrument.lbm", true) then + -- Wrap register_lbm() to automatically instrument lbms. + local orig_register_lbm = core.register_lbm + core.register_lbm = function(spec) + spec.action = instrument { + func = spec.action, + class = "LBM", + label = spec.label or spec.name, + } + orig_register_lbm(spec) + end + end + + if get_bool_default("instrument.global_callback", true) then + for func_name, _ in pairs(register_functions) do + core[func_name] = instrument_register(core[func_name], func_name) + end + end + + if get_bool_default("instrument.profiler", false) then + -- Measure overhead of instrumentation, but keep it down for functions + -- So keep the `return` for better optimization. + profiler.empty_instrument = instrument { + func = function() return end, + mod = "*profiler*", + class = "Instrumentation overhead", + label = false, + } + end +end + +return { + register_functions = register_functions, + instrument = instrument, + init = init, + init_chatcommand = init_chatcommand, +} diff --git a/builtin/profiler/reporter.lua b/builtin/profiler/reporter.lua new file mode 100644 index 0000000..5928a37 --- /dev/null +++ b/builtin/profiler/reporter.lua @@ -0,0 +1,280 @@ +--Minetest +--Copyright (C) 2016 T4im +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +local S = core.get_translator("__builtin") +-- Note: In this file, only messages are translated +-- but not the table itself, to keep it simple. + +local DIR_DELIM, LINE_DELIM = DIR_DELIM, "\n" +local table, unpack, string, pairs, io, os = table, unpack, string, pairs, io, os +local rep, sprintf, tonumber = string.rep, string.format, tonumber +local core, settings = core, core.settings +local reporter = {} + +--- +-- Shorten a string. End on an ellipsis if shortened. +-- +local function shorten(str, length) + if str and str:len() > length then + return "..." .. str:sub(-(length-3)) + end + return str +end + +local function filter_matches(filter, text) + return not filter or string.match(text, filter) +end + +local function format_number(number, fmt) + number = tonumber(number) + if not number then + return "N/A" + end + return sprintf(fmt or "%d", number) +end + +local Formatter = { + new = function(self, object) + object = object or {} + object.out = {} -- output buffer + self.__index = self + return setmetatable(object, self) + end, + __tostring = function (self) + return table.concat(self.out, LINE_DELIM) + end, + print = function(self, text, ...) + if (...) then + text = sprintf(text, ...) + end + + if text then + -- Avoid format unicode issues. + text = text:gsub("Ms", "µs") + end + + table.insert(self.out, text or LINE_DELIM) + end, + flush = function(self) + table.insert(self.out, LINE_DELIM) + local text = table.concat(self.out, LINE_DELIM) + self.out = {} + return text + end +} + +local widths = { 55, 9, 9, 9, 5, 5, 5 } +local txt_row_format = sprintf(" %%-%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds | %%%ds", unpack(widths)) + +local HR = {} +for i=1, #widths do + HR[i]= rep("-", widths[i]) +end +-- ' | ' should break less with github than '-+-', when people are pasting there +HR = sprintf("-%s-", table.concat(HR, " | ")) + +local TxtFormatter = Formatter:new { + format_row = function(self, modname, instrument_name, statistics) + local label + if instrument_name then + label = shorten(instrument_name, widths[1] - 5) + label = sprintf(" - %s %s", label, rep(".", widths[1] - 5 - label:len())) + else -- Print mod_stats + label = shorten(modname, widths[1] - 2) .. ":" + end + + self:print(txt_row_format, label, + format_number(statistics.time_min), + format_number(statistics.time_max), + format_number(statistics:get_time_avg()), + format_number(statistics.part_min, "%.1f"), + format_number(statistics.part_max, "%.1f"), + format_number(statistics:get_part_avg(), "%.1f") + ) + end, + format = function(self, filter) + local profile = self.profile + self:print(S("Values below show absolute/relative times spend per server step by the instrumented function.")) + self:print(S("A total of @1 sample(s) were taken.", profile.stats_total.samples)) + + if filter then + self:print(S("The output is limited to '@1'.", filter)) + end + + self:print() + self:print( + txt_row_format, + "instrumentation", "min Ms", "max Ms", "avg Ms", "min %", "max %", "avg %" + ) + self:print(HR) + for modname,mod_stats in pairs(profile.stats) do + if filter_matches(filter, modname) then + self:format_row(modname, nil, mod_stats) + + if mod_stats.instruments ~= nil then + for instrument_name, instrument_stats in pairs(mod_stats.instruments) do + self:format_row(nil, instrument_name, instrument_stats) + end + end + end + end + self:print(HR) + if not filter then + self:format_row("total", nil, profile.stats_total) + end + end +} + +local CsvFormatter = Formatter:new { + format_row = function(self, modname, instrument_name, statistics) + self:print( + "%q,%q,%d,%d,%d,%d,%d,%f,%f,%f", + modname, instrument_name, + statistics.samples, + statistics.time_min, + statistics.time_max, + statistics:get_time_avg(), + statistics.time_all, + statistics.part_min, + statistics.part_max, + statistics:get_part_avg() + ) + end, + format = function(self, filter) + self:print( + "%q,%q,%q,%q,%q,%q,%q,%q,%q,%q", + "modname", "instrumentation", + "samples", + "time min µs", + "time max µs", + "time avg µs", + "time all µs", + "part min %", + "part max %", + "part avg %" + ) + for modname, mod_stats in pairs(self.profile.stats) do + if filter_matches(filter, modname) then + self:format_row(modname, "*", mod_stats) + + if mod_stats.instruments ~= nil then + for instrument_name, instrument_stats in pairs(mod_stats.instruments) do + self:format_row(modname, instrument_name, instrument_stats) + end + end + end + end + end +} + +local function format_statistics(profile, format, filter) + local formatter + if format == "csv" then + formatter = CsvFormatter:new { + profile = profile + } + else + formatter = TxtFormatter:new { + profile = profile + } + end + formatter:format(filter) + return formatter:flush() +end + +--- +-- Format the profile ready for display and +-- @return string to be printed to the console +-- +function reporter.print(profile, filter) + if filter == "" then filter = nil end + return format_statistics(profile, "txt", filter) +end + +--- +-- Serialize the profile data and +-- @return serialized data to be saved to a file +-- +local function serialize_profile(profile, format, filter) + if format == "lua" or format == "json" or format == "json_pretty" then + local stats = filter and {} or profile.stats + if filter then + for modname, mod_stats in pairs(profile.stats) do + if filter_matches(filter, modname) then + stats[modname] = mod_stats + end + end + end + if format == "lua" then + return core.serialize(stats) + elseif format == "json" then + return core.write_json(stats) + elseif format == "json_pretty" then + return core.write_json(stats, true) + end + end + -- Fall back to textual formats. + return format_statistics(profile, format, filter) +end + +local worldpath = core.get_worldpath() +local function get_save_path(format, filter) + local report_path = settings:get("profiler.report_path") or "" + if report_path ~= "" then + core.mkdir(sprintf("%s%s%s", worldpath, DIR_DELIM, report_path)) + end + return (sprintf( + "%s/%s/profile-%s%s.%s", + worldpath, + report_path, + os.date("%Y%m%dT%H%M%S"), + filter and ("-" .. filter) or "", + format + ):gsub("[/\\]+", DIR_DELIM))-- Clean up delims +end + +--- +-- Save the profile to the world path. +-- @return success, log message +-- +function reporter.save(profile, format, filter) + if not format or format == "" then + format = settings:get("profiler.default_report_format") or "txt" + end + if filter == "" then + filter = nil + end + + local path = get_save_path(format, filter) + + local output, io_err = io.open(path, "w") + if not output then + return false, S("Saving of profile failed: @1", io_err) + end + local content, err = serialize_profile(profile, format, filter) + if not content then + output:close() + return false, S("Saving of profile failed: @1", err) + end + output:write(content) + output:close() + + core.log("action", "Profile saved to " .. path) + return true, S("Profile saved to @1", path) +end + +return reporter diff --git a/builtin/profiler/sampling.lua b/builtin/profiler/sampling.lua new file mode 100644 index 0000000..4b53399 --- /dev/null +++ b/builtin/profiler/sampling.lua @@ -0,0 +1,206 @@ +--Minetest +--Copyright (C) 2016 T4im +-- +--This program is free software; you can redistribute it and/or modify +--it under the terms of the GNU Lesser General Public License as published by +--the Free Software Foundation; either version 2.1 of the License, or +--(at your option) any later version. +-- +--This program is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU Lesser General Public License for more details. +-- +--You should have received a copy of the GNU Lesser General Public License along +--with this program; if not, write to the Free Software Foundation, Inc., +--51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +local setmetatable = setmetatable +local pairs, format = pairs, string.format +local min, max, huge = math.min, math.max, math.huge +local core = core + +local profiler = ... +-- Split sampler and profile up, to possibly allow for rotation later. +local sampler = {} +local profile +local stats_total +local logged_time, logged_data + +local _stat_mt = { + get_time_avg = function(self) + return self.time_all/self.samples + end, + get_part_avg = function(self) + if not self.part_all then + return 100 -- Extra handling for "total" + end + return self.part_all/self.samples + end, +} +_stat_mt.__index = _stat_mt + +function sampler.reset() + -- Accumulated logged time since last sample. + -- This helps determining, the relative time a mod used up. + logged_time = 0 + -- The measurements taken through instrumentation since last sample. + logged_data = {} + + profile = { + -- Current mod statistics (max/min over the entire mod lifespan) + -- Mod specific instrumentation statistics are nested within. + stats = {}, + -- Current stats over all mods. + stats_total = setmetatable({ + samples = 0, + time_min = huge, + time_max = 0, + time_all = 0, + part_min = 100, + part_max = 100 + }, _stat_mt) + } + stats_total = profile.stats_total + + -- Provide access to the most recent profile. + sampler.profile = profile +end + +--- +-- Log a measurement for the sampler to pick up later. +-- Keep `log` and its often called functions lean. +-- It will directly add to the instrumentation overhead. +-- +function sampler.log(modname, instrument_name, time_diff) + if time_diff <= 0 then + if time_diff < 0 then + -- This **might** have happened on a semi-regular basis with huge mods, + -- resulting in negative statistics (perhaps midnight time jumps or ntp corrections?). + core.log("warning", format( + "Time travel of %s::%s by %dµs.", + modname, instrument_name, time_diff + )) + end + -- Throwing these away is better, than having them mess with the overall result. + return + end + + local mod_data = logged_data[modname] + if mod_data == nil then + mod_data = {} + logged_data[modname] = mod_data + end + + mod_data[instrument_name] = (mod_data[instrument_name] or 0) + time_diff + -- Update logged time since last sample. + logged_time = logged_time + time_diff +end + +--- +-- Return a requested statistic. +-- Initialize if necessary. +-- +local function get_statistic(stats_table, name) + local statistic = stats_table[name] + if statistic == nil then + statistic = setmetatable({ + samples = 0, + time_min = huge, + time_max = 0, + time_all = 0, + part_min = 100, + part_max = 0, + part_all = 0, + }, _stat_mt) + stats_table[name] = statistic + end + return statistic +end + +--- +-- Update a statistic table +-- +local function update_statistic(stats_table, time) + stats_table.samples = stats_table.samples + 1 + + -- Update absolute time (µs) spend by the subject + stats_table.time_min = min(stats_table.time_min, time) + stats_table.time_max = max(stats_table.time_max, time) + stats_table.time_all = stats_table.time_all + time + + -- Update relative time (%) of this sample spend by the subject + local current_part = (time/logged_time) * 100 + stats_table.part_min = min(stats_table.part_min, current_part) + stats_table.part_max = max(stats_table.part_max, current_part) + stats_table.part_all = stats_table.part_all + current_part +end + +--- +-- Sample all logged measurements each server step. +-- Like any globalstep function, this should not be too heavy, +-- but does not add to the instrumentation overhead. +-- +local function sample(dtime) + -- Rare, but happens and is currently of no informational value. + if logged_time == 0 then + return + end + + for modname, instruments in pairs(logged_data) do + local mod_stats = get_statistic(profile.stats, modname) + if mod_stats.instruments == nil then + -- Current statistics for each instrumentation component + mod_stats.instruments = {} + end + + local mod_time = 0 + for instrument_name, time in pairs(instruments) do + if time > 0 then + mod_time = mod_time + time + local instrument_stats = get_statistic(mod_stats.instruments, instrument_name) + + -- Update time of this sample spend by the instrumented function. + update_statistic(instrument_stats, time) + -- Reset logged data for the next sample. + instruments[instrument_name] = 0 + end + end + + -- Update time of this sample spend by this mod. + update_statistic(mod_stats, mod_time) + end + + -- Update the total time spend over all mods. + stats_total.time_min = min(stats_total.time_min, logged_time) + stats_total.time_max = max(stats_total.time_max, logged_time) + stats_total.time_all = stats_total.time_all + logged_time + + stats_total.samples = stats_total.samples + 1 + logged_time = 0 +end + +--- +-- Setup empty profile and register the sampling function +-- +function sampler.init() + sampler.reset() + + if core.settings:get_bool("instrument.profiler") then + core.register_globalstep(function() + if logged_time == 0 then + return + end + return profiler.empty_instrument() + end) + core.register_globalstep(profiler.instrument { + func = sample, + mod = "*profiler*", + class = "Sampler (update stats)", + label = false, + }) + else + core.register_globalstep(sample) + end +end + +return sampler diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt new file mode 100644 index 0000000..52b4b4d --- /dev/null +++ b/builtin/settingtypes.txt @@ -0,0 +1,2353 @@ +# This file contains all settings displayed in the settings menu. +# +# General format: +# name (Readable name) type type_args +# +# Note that the parts are separated by exactly one space +# +# `type` can be: +# - int +# - string +# - bool +# - float +# - enum +# - path +# - filepath +# - key (will be ignored in GUI, since a special key change dialog exists) +# - flags +# - noise_params_2d +# - noise_params_3d +# - v3f +# +# `type_args` can be: +# * int: +# - default +# - default min max +# * string: +# - default (if default is not specified then "" is set) +# * bool: +# - default +# * float: +# - default +# - default min max +# * enum: +# - default value1,value2,... +# * path: +# - default (if default is not specified then "" is set) +# * filepath: +# - default (if default is not specified then "" is set) +# * key: +# - default +# * flags: +# Flags are always separated by comma without spaces. +# - default possible_flags +# * noise_params_2d: +# Format is , , (, , ), , , , [, ] +# - default +# * noise_params_3d: +# Format is , , (, , ), , , , [, ] +# - default +# * v3f: +# Format is (, , ) +# - default +# +# Comments directly above a setting are bound to this setting. +# All other comments are ignored. +# +# Comments and (Readable name) are handled by gettext. +# Comments should be complete sentences that describe the setting and possibly +# give the user additional useful insight. +# Sections are marked by a single line in the format: [Section Name] +# Sub-section are marked by adding * in front of the section name: [*Sub-section] +# Sub-sub-sections have two * etc. +# There shouldn't be too much settings per category; settings that shouldn't be +# modified by the "average user" should be in (sub-)categories called "Advanced". + + +[Controls] + +[*General] + +# If enabled, you can place blocks at the position (feet + eye level) where you stand. +# This is helpful when working with nodeboxes in small areas. +enable_build_where_you_stand (Build inside player) bool false + +# Smooths camera when looking around. Also called look or mouse smoothing. +# Useful for recording videos. +cinematic (Cinematic mode) bool false + +# Smooths rotation of camera. 0 to disable. +camera_smoothing (Camera smoothing) float 0.0 0.0 0.99 + +# Smooths rotation of camera in cinematic mode. 0 to disable. +cinematic_camera_smoothing (Camera smoothing in cinematic mode) float 0.7 0.0 0.99 + +# If enabled, "Aux1" key instead of "Sneak" key is used for climbing down and +# descending. +aux1_descends (Aux1 key for climbing/descending) bool false + +# Double-tapping the jump key toggles fly mode. +doubletap_jump (Double tap jump for fly) bool false + +# If disabled, "Aux1" key is used to fly fast if both fly and fast mode are +# enabled. +always_fly_fast (Always fly fast) bool true + +# The time in seconds it takes between repeated node placements when holding +# the place button. +repeat_place_time (Place repetition interval) float 0.25 0.25 2 + +# Automatically jump up single-node obstacles. +autojump (Automatic jumping) bool false + +# Prevent digging and placing from repeating when holding the mouse buttons. +# Enable this when you dig or place too often by accident. +safe_dig_and_place (Safe digging and placing) bool false + +[*Keyboard and Mouse] + +# Invert vertical mouse movement. +invert_mouse (Invert mouse) bool false + +# Mouse sensitivity multiplier. +mouse_sensitivity (Mouse sensitivity) float 0.2 0.001 10.0 + +[*Touchscreen] + +# The length in pixels it takes for touch screen interaction to start. +touchscreen_threshold (Touch screen threshold) int 20 0 100 + +# (Android) Fixes the position of virtual joystick. +# If disabled, virtual joystick will center to first-touch's position. +fixed_virtual_joystick (Fixed virtual joystick) bool false + +# (Android) Use virtual joystick to trigger "Aux1" button. +# If enabled, virtual joystick will also tap "Aux1" button when out of main circle. +virtual_joystick_triggers_aux1 (Virtual joystick triggers Aux1 button) bool false + + +[Graphics and Audio] + +[*Graphics] + +[**Screen] + +# Width component of the initial window size. Ignored in fullscreen mode. +screen_w (Screen width) int 1024 1 65535 + +# Height component of the initial window size. Ignored in fullscreen mode. +screen_h (Screen height) int 600 1 65535 + +# Save window size automatically when modified. +autosave_screensize (Autosave screen size) bool true + +# Fullscreen mode. +fullscreen (Full screen) bool false + +# Open the pause menu when the window's focus is lost. Does not pause if a formspec is +# open. +pause_on_lost_focus (Pause on lost window focus) bool false + +[**FPS] + +# If FPS would go higher than this, limit it by sleeping +# to not waste CPU power for no benefit. +fps_max (Maximum FPS) int 60 1 4294967295 + +# Vertical screen synchronization. +vsync (VSync) bool false + +# Maximum FPS when the window is not focused, or when the game is paused. +fps_max_unfocused (FPS when unfocused or paused) int 20 1 4294967295 + +# View distance in nodes. +viewing_range (Viewing range) int 190 20 4000 + +# Undersampling is similar to using a lower screen resolution, but it applies +# to the game world only, keeping the GUI intact. +# It should give a significant performance boost at the cost of less detailed image. +# Higher values result in a less detailed image. +undersampling (Undersampling) int 1 1 8 + +[**Graphics Effects] + +# Makes all liquids opaque +opaque_water (Opaque liquids) bool false + +# Leaves style: +# - Fancy: all faces visible +# - Simple: only outer faces, if defined special_tiles are used +# - Opaque: disable transparency +leaves_style (Leaves style) enum fancy fancy,simple,opaque + +# Connects glass if supported by node. +connected_glass (Connect glass) bool false + +# Enable smooth lighting with simple ambient occlusion. +# Disable for speed or for different looks. +smooth_lighting (Smooth lighting) bool true + +# Enables tradeoffs that reduce CPU load or increase rendering performance +# at the expense of minor visual glitches that do not impact game playability. +performance_tradeoffs (Tradeoffs for performance) bool false + +# Adds particles when digging a node. +enable_particles (Digging particles) bool true + +[**3d] + +# 3D support. +# Currently supported: +# - none: no 3d output. +# - anaglyph: cyan/magenta color 3d. +# - interlaced: odd/even line based polarisation screen support. +# - topbottom: split screen top/bottom. +# - sidebyside: split screen side by side. +# - crossview: Cross-eyed 3d +# - pageflip: quadbuffer based 3d. +# Note that the interlaced mode requires shaders to be enabled. +3d_mode (3D mode) enum none none,anaglyph,interlaced,topbottom,sidebyside,crossview,pageflip + +# Strength of 3D mode parallax. +3d_paralax_strength (3D mode parallax strength) float 0.025 -0.087 0.087 + +[**Bobbing] + +# Arm inertia, gives a more realistic movement of +# the arm when the camera moves. +arm_inertia (Arm inertia) bool true + +# Enable view bobbing and amount of view bobbing. +# For example: 0 for no view bobbing; 1.0 for normal; 2.0 for double. +view_bobbing_amount (View bobbing factor) float 1.0 0.0 7.9 + +# Multiplier for fall bobbing. +# For example: 0 for no view bobbing; 1.0 for normal; 2.0 for double. +fall_bobbing_amount (Fall bobbing factor) float 0.03 0.0 100.0 + +[**Camera] + +# Camera 'near clipping plane' distance in nodes, between 0 and 0.25 +# Only works on GLES platforms. Most users will not need to change this. +# Increasing can reduce artifacting on weaker GPUs. +# 0.1 = Default, 0.25 = Good value for weaker tablets. +near_plane (Near plane) float 0.1 0 0.25 + +# Field of view in degrees. +fov (Field of view) int 72 45 160 + +# Alters the light curve by applying 'gamma correction' to it. +# Higher values make middle and lower light levels brighter. +# Value '1.0' leaves the light curve unaltered. +# This only has significant effect on daylight and artificial +# light, it has very little effect on natural night light. +display_gamma (Light curve gamma) float 1.0 0.33 3.0 + +# The strength (darkness) of node ambient-occlusion shading. +# Lower is darker, Higher is lighter. The valid range of values for this +# setting is 0.25 to 4.0 inclusive. If the value is out of range it will be +# set to the nearest valid value. +ambient_occlusion_gamma (Ambient occlusion gamma) float 2.2 0.25 4.0 + +[**Screenshots] + +# Path to save screenshots at. Can be an absolute or relative path. +# The folder will be created if it doesn't already exist. +screenshot_path (Screenshot folder) path screenshots + +# Format of screenshots. +screenshot_format (Screenshot format) enum png png,jpg + +# Screenshot quality. Only used for JPEG format. +# 1 means worst quality; 100 means best quality. +# Use 0 for default quality. +screenshot_quality (Screenshot quality) int 0 0 100 + +[**Node and Entity Highlighting] + +# Method used to highlight selected object. +node_highlighting (Node highlighting) enum box box,halo,none + +# Show entity selection boxes +# A restart is required after changing this. +show_entity_selectionbox (Show entity selection boxes) bool false + +# Selection box border color (R,G,B). +selectionbox_color (Selection box color) string (0,0,0) + +# Width of the selection box lines around nodes. +selectionbox_width (Selection box width) int 2 1 5 + +# Crosshair color (R,G,B). +# Also controls the object crosshair color +crosshair_color (Crosshair color) string (255,255,255) + +# Crosshair alpha (opaqueness, between 0 and 255). +# This also applies to the object crosshair. +crosshair_alpha (Crosshair alpha) int 255 0 255 + +[**Fog] + +# Whether to fog out the end of the visible area. +enable_fog (Fog) bool true + +# Make fog and sky colors depend on daytime (dawn/sunset) and view direction. +directional_colored_fog (Colored fog) bool true + +# Fraction of the visible distance at which fog starts to be rendered +fog_start (Fog start) float 0.4 0.0 0.99 + +[**Clouds] + +# Clouds are a client side effect. +enable_clouds (Clouds) bool true + +# Use 3D cloud look instead of flat. +enable_3d_clouds (3D clouds) bool true + +[**Filtering and Antialiasing] + +# Use mipmapping to scale textures. May slightly increase performance, +# especially when using a high resolution texture pack. +# Gamma correct downscaling is not supported. +mip_map (Mipmapping) bool false + +# Use anisotropic filtering when viewing at textures from an angle. +anisotropic_filter (Anisotropic filtering) bool false + +# Use bilinear filtering when scaling textures. +bilinear_filter (Bilinear filtering) bool false + +# Use trilinear filtering when scaling textures. +trilinear_filter (Trilinear filtering) bool false + +# Filtered textures can blend RGB values with fully-transparent neighbors, +# which PNG optimizers usually discard, often resulting in dark or +# light edges to transparent textures. Apply a filter to clean that up +# at texture load time. This is automatically enabled if mipmapping is enabled. +texture_clean_transparent (Clean transparent textures) bool false + +# When using bilinear/trilinear/anisotropic filters, low-resolution textures +# can be blurred, so automatically upscale them with nearest-neighbor +# interpolation to preserve crisp pixels. This sets the minimum texture size +# for the upscaled textures; higher values look sharper, but require more +# memory. Powers of 2 are recommended. This setting is ONLY applied if +# bilinear/trilinear/anisotropic filtering is enabled. +# This is also used as the base node texture size for world-aligned +# texture autoscaling. +texture_min_size (Minimum texture size) int 64 1 32768 + +# Use multi-sample antialiasing (MSAA) to smooth out block edges. +# This algorithm smooths out the 3D viewport while keeping the image sharp, +# but it doesn't affect the insides of textures +# (which is especially noticeable with transparent textures). +# Visible spaces appear between nodes when shaders are disabled. +# If set to 0, MSAA is disabled. +# A restart is required after changing this option. +fsaa (FSAA) enum 0 0,1,2,4,8,16 + + +[*Shaders] + +# Shaders allow advanced visual effects and may increase performance on some video +# cards. +# This only works with the OpenGL video backend. +enable_shaders (Shaders) bool true + +[**Tone Mapping] + +# Enables Hable's 'Uncharted 2' filmic tone mapping. +# Simulates the tone curve of photographic film and how this approximates the +# appearance of high dynamic range images. Mid-range contrast is slightly +# enhanced, highlights and shadows are gradually compressed. +tone_mapping (Filmic tone mapping) bool false + +[**Waving Nodes] + +# Set to true to enable waving leaves. +# Requires shaders to be enabled. +enable_waving_leaves (Waving leaves) bool false + +# Set to true to enable waving plants. +# Requires shaders to be enabled. +enable_waving_plants (Waving plants) bool false + +# Set to true to enable waving liquids (like water). +# Requires shaders to be enabled. +enable_waving_water (Waving liquids) bool false + +# The maximum height of the surface of waving liquids. +# 4.0 = Wave height is two nodes. +# 0.0 = Wave doesn't move at all. +# Default is 1.0 (1/2 node). +# Requires waving liquids to be enabled. +water_wave_height (Waving liquids wave height) float 1.0 0.0 4.0 + +# Length of liquid waves. +# Requires waving liquids to be enabled. +water_wave_length (Waving liquids wavelength) float 20.0 0.1 + +# How fast liquid waves will move. Higher = faster. +# If negative, liquid waves will move backwards. +# Requires waving liquids to be enabled. +water_wave_speed (Waving liquids wave speed) float 5.0 + +[**Dynamic shadows] + +# Set to true to enable Shadow Mapping. +# Requires shaders to be enabled. +enable_dynamic_shadows (Dynamic shadows) bool false + +# Set the shadow strength gamma. +# Adjusts the intensity of in-game dynamic shadows. +# Lower value means lighter shadows, higher value means darker shadows. +shadow_strength_gamma (Shadow strength gamma) float 1.0 0.1 10.0 + +# Maximum distance to render shadows. +shadow_map_max_distance (Shadow map max distance in nodes to render shadows) float 120.0 10.0 1000.0 + +# Texture size to render the shadow map on. +# This must be a power of two. +# Bigger numbers create better shadows but it is also more expensive. +shadow_map_texture_size (Shadow map texture size) int 1024 128 8192 + +# Sets shadow texture quality to 32 bits. +# On false, 16 bits texture will be used. +# This can cause much more artifacts in the shadow. +shadow_map_texture_32bit (Shadow map texture in 32 bits) bool true + +# Enable Poisson disk filtering. +# On true uses Poisson disk to make "soft shadows". Otherwise uses PCF filtering. +shadow_poisson_filter (Poisson filtering) bool true + +# Define shadow filtering quality. +# This simulates the soft shadows effect by applying a PCF or Poisson disk +# but also uses more resources. +shadow_filters (Shadow filter quality) enum 1 0,1,2 + +# Enable colored shadows. +# On true translucent nodes cast colored shadows. This is expensive. +shadow_map_color (Colored shadows) bool false + +# Spread a complete update of shadow map over given amount of frames. +# Higher values might make shadows laggy, lower values +# will consume more resources. +# Minimum value: 1; maximum value: 16 +shadow_update_frames (Map shadows update frames) int 8 1 16 + +# Set the soft shadow radius size. +# Lower values mean sharper shadows, bigger values mean softer shadows. +# Minimum value: 1.0; maximum value: 15.0 +shadow_soft_radius (Soft shadow radius) float 5.0 1.0 15.0 + +# Set the tilt of Sun/Moon orbit in degrees. +# Value of 0 means no tilt / vertical orbit. +# Minimum value: 0.0; maximum value: 60.0 +shadow_sky_body_orbit_tilt (Sky Body Orbit Tilt) float 0.0 0.0 60.0 + +[*Audio] + +# Volume of all sounds. +# Requires the sound system to be enabled. +sound_volume (Volume) float 0.7 0.0 1.0 + +# Whether to mute sounds. You can unmute sounds at any time, unless the +# sound system is disabled (enable_sound=false). +# In-game, you can toggle the mute state with the mute key or by using the +# pause menu. +mute_sound (Mute sound) bool false + +[*User Interfaces] + +# Set the language. Leave empty to use the system language. +# A restart is required after changing this. +language (Language) enum ,be,bg,ca,cs,da,de,el,en,eo,es,et,eu,fi,fr,gd,gl,hu,id,it,ja,jbo,kk,ko,lt,lv,ms,nb,nl,nn,pl,pt,pt_BR,ro,ru,sk,sl,sr_Cyrl,sr_Latn,sv,sw,tr,uk,vi,zh_CN,zh_TW + +[**GUIs] + +# Scale GUI by a user specified value. +# Use a nearest-neighbor-anti-alias filter to scale the GUI. +# This will smooth over some of the rough edges, and blend +# pixels when scaling down, at the cost of blurring some +# edge pixels when images are scaled by non-integer sizes. +gui_scaling (GUI scaling) float 1.0 0.5 20 + +# Enables animation of inventory items. +inventory_items_animations (Inventory items animations) bool false + +# Formspec full-screen background opacity (between 0 and 255). +formspec_fullscreen_bg_opacity (Formspec Full-Screen Background Opacity) int 140 0 255 + +# Formspec full-screen background color (R,G,B). +formspec_fullscreen_bg_color (Formspec Full-Screen Background Color) string (0,0,0) + +# When gui_scaling_filter is true, all GUI images need to be +# filtered in software, but some images are generated directly +# to hardware (e.g. render-to-texture for nodes in inventory). +gui_scaling_filter (GUI scaling filter) bool false + +# When gui_scaling_filter_txr2img is true, copy those images +# from hardware to software for scaling. When false, fall back +# to the old scaling method, for video drivers that don't +# properly support downloading textures back from hardware. +gui_scaling_filter_txr2img (GUI scaling filter txr2img) bool true + +# Delay showing tooltips, stated in milliseconds. +tooltip_show_delay (Tooltip delay) int 400 0 18446744073709551615 + +# Append item name to tooltip. +tooltip_append_itemname (Append item name) bool false + +# Use a cloud animation for the main menu background. +menu_clouds (Clouds in menu) bool true + +[**HUD] + +# Modifies the size of the HUD elements. +hud_scaling (HUD scaling) float 1.0 0.5 20 + +# Whether name tag backgrounds should be shown by default. +# Mods may still set a background. +show_nametag_backgrounds (Show name tag backgrounds by default) bool true + +[**Chat] + +# Maximum number of recent chat messages to show +recent_chat_messages (Recent Chat Messages) int 6 2 20 + +# In-game chat console height, between 0.1 (10%) and 1.0 (100%). +console_height (Console height) float 0.6 0.1 1.0 + +# In-game chat console background color (R,G,B). +console_color (Console color) string (0,0,0) + +# In-game chat console background alpha (opaqueness, between 0 and 255). +console_alpha (Console alpha) int 200 0 255 + +# Maximum proportion of current window to be used for hotbar. +# Useful if there's something to be displayed right or left of hotbar. +hud_hotbar_max_width (Maximum hotbar width) float 1.0 0.001 1.0 + +# Clickable weblinks (middle-click or Ctrl+left-click) enabled in chat console output. +clickable_chat_weblinks (Chat weblinks) bool true + +# Optional override for chat weblink color. +chat_weblink_color (Weblink color) string + +# Font size of the recent chat text and chat prompt in point (pt). +# Value 0 will use the default font size. +chat_font_size (Chat font size) int 0 0 72 + + +[**Content Repository] + +# The URL for the content repository +contentdb_url (ContentDB URL) string https://content.minetest.net + +# Comma-separated list of flags to hide in the content repository. +# "nonfree" can be used to hide packages which do not qualify as 'free software', +# as defined by the Free Software Foundation. +# You can also specify content ratings. +# These flags are independent from Minetest versions, +# so see a full list at https://content.minetest.net/help/content_flags/ +contentdb_flag_blacklist (ContentDB Flag Blacklist) string nonfree, desktop_default + +# Maximum number of concurrent downloads. Downloads exceeding this limit will be queued. +# This should be lower than curl_parallel_limit. +contentdb_max_concurrent_downloads (ContentDB Max Concurrent Downloads) int 3 1 + + +[Client and Server] + +[*Client] + +# Save the map received by the client on disk. +enable_local_map_saving (Saving map received from server) bool false + +# URL to the server list displayed in the Multiplayer Tab. +serverlist_url (Serverlist URL) string servers.minetest.net + +# If enabled, account registration is separate from login in the UI. +# If disabled, new accounts will be registered automatically when logging in. +enable_split_login_register (Enable split login/register) bool true + +# URL to JSON file which provides information about the newest Minetest release +update_information_url (Update information URL) string https://www.minetest.net/release_info.json + +# Unix timestamp (integer) of when the client last checked for an update +# Set this value to "disabled" to never check for updates. +update_last_checked (Last update check) string + +# Version number which was last seen during an update check. +# +# Representation: MMMIIIPPP, where M=Major, I=Minor, P=Patch +# Ex: 5.5.0 is 005005000 +update_last_known (Last known version update) int 0 + +[*Server] + +# Name of the player. +# When running a server, clients connecting with this name are admins. +# When starting from the main menu, this is overridden. +name (Admin name) string + +[**Serverlist and MOTD] + +# Name of the server, to be displayed when players join and in the serverlist. +server_name (Server name) string Minetest server + +# Description of server, to be displayed when players join and in the serverlist. +server_description (Server description) string mine here + +# Domain name of server, to be displayed in the serverlist. +server_address (Server address) string game.minetest.net + +# Homepage of server, to be displayed in the serverlist. +server_url (Server URL) string https://minetest.net + +# Automatically report to the serverlist. +server_announce (Announce server) bool false + +# Announce to this serverlist. +serverlist_url (Serverlist URL) string servers.minetest.net + +# Message of the day displayed to players connecting. +motd (Message of the day) string + +# Maximum number of players that can be connected simultaneously. +max_users (Maximum users) int 15 0 65535 + +# If this is set, players will always (re)spawn at the given position. +static_spawnpoint (Static spawnpoint) string + +[**Networking] + +# Network port to listen (UDP). +# This value will be overridden when starting from the main menu. +port (Server port) int 30000 1 65535 + +# The network interface that the server listens on. +bind_address (Bind address) string + +# Enable to disallow old clients from connecting. +# Older clients are compatible in the sense that they will not crash when connecting +# to new servers, but they may not support all new features that you are expecting. +strict_protocol_version_checking (Strict protocol checking) bool false + +# Specifies URL from which client fetches media instead of using UDP. +# $filename should be accessible from $remote_media$filename via cURL +# (obviously, remote_media should end with a slash). +# Files that are not present will be fetched the usual way. +remote_media (Remote media) string + +# Enable/disable running an IPv6 server. +# Ignored if bind_address is set. +# Needs enable_ipv6 to be enabled. +ipv6_server (IPv6 server) bool false + +[*Server Security] + +# New users need to input this password. +default_password (Default password) string + +# If enabled, players cannot join without a password or change theirs to an empty password. +disallow_empty_password (Disallow empty passwords) bool false + +# The privileges that new users automatically get. +# See /privs in game for a full list on your server and mod configuration. +default_privs (Default privileges) string interact, shout + +# Privileges that players with basic_privs can grant +basic_privs (Basic privileges) string interact, shout + +# If enabled, disable cheat prevention in multiplayer. +disable_anticheat (Disable anticheat) bool false + +# If enabled, actions are recorded for rollback. +# This option is only read when server starts. +enable_rollback_recording (Rollback recording) bool false + +[**Client-side Modding] + +# Restricts the access of certain client-side functions on servers. +# Combine the byteflags below to restrict client-side features, or set to 0 +# for no restrictions: +# LOAD_CLIENT_MODS: 1 (disable loading client-provided mods) +# CHAT_MESSAGES: 2 (disable send_chat_message call client-side) +# READ_ITEMDEFS: 4 (disable get_item_def call client-side) +# READ_NODEDEFS: 8 (disable get_node_def call client-side) +# LOOKUP_NODES_LIMIT: 16 (limits get_node call client-side to +# csm_restriction_noderange) +# READ_PLAYERINFO: 32 (disable get_player_names call client-side) +csm_restriction_flags (Client side modding restrictions) int 62 0 63 + +# If the CSM restriction for node range is enabled, get_node calls are limited +# to this distance from the player to the node. +csm_restriction_noderange (Client side node lookup range restriction) int 0 0 4294967295 + +[**Chat] + +# Remove color codes from incoming chat messages +# Use this to stop players from being able to use color in their messages +strip_color_codes (Strip color codes) bool false + +# Set the maximum length of a chat message (in characters) sent by clients. +chat_message_max_size (Chat message max length) int 500 10 65535 + +# Amount of messages a player may send per 10 seconds. +chat_message_limit_per_10sec (Chat message count limit) float 10.0 1.0 + +# Kick players who sent more than X messages per 10 seconds. +chat_message_limit_trigger_kick (Chat message kick threshold) int 50 1 65535 + +[*Server Gameplay] + +# Controls length of day/night cycle. +# Examples: +# 72 = 20min, 360 = 4min, 1 = 24hour, 0 = day/night/whatever stays unchanged. +time_speed (Time speed) int 72 0 + +# Time of day when a new world is started, in millihours (0-23999). +world_start_time (World start time) int 6125 0 23999 + +# Time in seconds for item entity (dropped items) to live. +# Setting it to -1 disables the feature. +item_entity_ttl (Item entity TTL) int 900 -1 + +# Specifies the default stack size of nodes, items and tools. +# Note that mods or games may explicitly set a stack for certain (or all) items. +default_stack_max (Default stack size) int 99 1 65535 + +[**Physics] + +# Horizontal and vertical acceleration on ground or when climbing, +# in nodes per second per second. +movement_acceleration_default (Default acceleration) float 3.0 0.0 + +# Horizontal acceleration in air when jumping or falling, +# in nodes per second per second. +movement_acceleration_air (Acceleration in air) float 2.0 0.0 + +# Horizontal and vertical acceleration in fast mode, +# in nodes per second per second. +movement_acceleration_fast (Fast mode acceleration) float 10.0 0.0 + +# Walking and flying speed, in nodes per second. +movement_speed_walk (Walking speed) float 4.0 0.0 + +# Sneaking speed, in nodes per second. +movement_speed_crouch (Sneaking speed) float 1.35 0.0 + +# Walking, flying and climbing speed in fast mode, in nodes per second. +movement_speed_fast (Fast mode speed) float 20.0 0.0 + +# Vertical climbing speed, in nodes per second. +movement_speed_climb (Climbing speed) float 3.0 0.0 + +# Initial vertical speed when jumping, in nodes per second. +movement_speed_jump (Jumping speed) float 6.5 0.0 + +# How much you are slowed down when moving inside a liquid. +# Decrease this to increase liquid resistance to movement. +movement_liquid_fluidity (Liquid fluidity) float 1.0 0.001 + +# Maximum liquid resistance. Controls deceleration when entering liquid at +# high speed. +movement_liquid_fluidity_smooth (Liquid fluidity smoothing) float 0.5 + +# Controls sinking speed in liquid when idling. Negative values will cause +# you to rise instead. +movement_liquid_sink (Liquid sinking) float 10.0 + +# Acceleration of gravity, in nodes per second per second. +movement_gravity (Gravity) float 9.81 + + +[Mapgen] + +# A chosen map seed for a new map, leave empty for random. +# Will be overridden when creating a new world in the main menu. +fixed_map_seed (Fixed map seed) string + +# Name of map generator to be used when creating a new world. +# Creating a world in the main menu will override this. +# Current mapgens in a highly unstable state: +# - The optional floatlands of v7 (disabled by default). +mg_name (Mapgen name) enum v7 v7,valleys,carpathian,v5,flat,fractal,singlenode,v6 + +# Water surface level of the world. +water_level (Water level) int 1 -31000 31000 + +# From how far blocks are generated for clients, stated in mapblocks (16 nodes). +max_block_generate_distance (Max block generate distance) int 10 1 32767 + +# Limit of map generation, in nodes, in all 6 directions from (0, 0, 0). +# Only mapchunks completely within the mapgen limit are generated. +# Value is stored per-world. +mapgen_limit (Map generation limit) int 31007 0 31007 + +# Global map generation attributes. +# In Mapgen v6 the 'decorations' flag controls all decorations except trees +# and jungle grass, in all other mapgens this flag controls all decorations. +mg_flags (Mapgen flags) flags caves,dungeons,light,decorations,biomes,ores caves,dungeons,light,decorations,biomes,ores,nocaves,nodungeons,nolight,nodecorations,nobiomes,noores + +[*Biome API noise parameters] + +# Temperature variation for biomes. +mg_biome_np_heat (Heat noise) noise_params_2d 50, 50, (1000, 1000, 1000), 5349, 3, 0.5, 2.0, eased + +# Small-scale temperature variation for blending biomes on borders. +mg_biome_np_heat_blend (Heat blend noise) noise_params_2d 0, 1.5, (8, 8, 8), 13, 2, 1.0, 2.0, eased + +# Humidity variation for biomes. +mg_biome_np_humidity (Humidity noise) noise_params_2d 50, 50, (1000, 1000, 1000), 842, 3, 0.5, 2.0, eased + +# Small-scale humidity variation for blending biomes on borders. +mg_biome_np_humidity_blend (Humidity blend noise) noise_params_2d 0, 1.5, (8, 8, 8), 90003, 2, 1.0, 2.0, eased + +[*Mapgen V5] + +# Map generation attributes specific to Mapgen v5. +mgv5_spflags (Mapgen V5 specific flags) flags caverns caverns,nocaverns + +# Controls width of tunnels, a smaller value creates wider tunnels. +# Value >= 10.0 completely disables generation of tunnels and avoids the +# intensive noise calculations. +mgv5_cave_width (Cave width) float 0.09 + +# Y of upper limit of large caves. +mgv5_large_cave_depth (Large cave depth) int -256 -31000 31000 + +# Minimum limit of random number of small caves per mapchunk. +mgv5_small_cave_num_min (Small cave minimum number) int 0 0 256 + +# Maximum limit of random number of small caves per mapchunk. +mgv5_small_cave_num_max (Small cave maximum number) int 0 0 256 + +# Minimum limit of random number of large caves per mapchunk. +mgv5_large_cave_num_min (Large cave minimum number) int 0 0 64 + +# Maximum limit of random number of large caves per mapchunk. +mgv5_large_cave_num_max (Large cave maximum number) int 2 0 64 + +# Proportion of large caves that contain liquid. +mgv5_large_cave_flooded (Large cave proportion flooded) float 0.5 0.0 1.0 + +# Y-level of cavern upper limit. +mgv5_cavern_limit (Cavern limit) int -256 -31000 31000 + +# Y-distance over which caverns expand to full size. +mgv5_cavern_taper (Cavern taper) int 256 0 32767 + +# Defines full size of caverns, smaller values create larger caverns. +mgv5_cavern_threshold (Cavern threshold) float 0.7 + +# Lower Y limit of dungeons. +mgv5_dungeon_ymin (Dungeon minimum Y) int -31000 -31000 31000 + +# Upper Y limit of dungeons. +mgv5_dungeon_ymax (Dungeon maximum Y) int 31000 -31000 31000 + +[**Noises] + +# Variation of biome filler depth. +mgv5_np_filler_depth (Filler depth noise) noise_params_2d 0, 1, (150, 150, 150), 261, 4, 0.7, 2.0, eased + +# Variation of terrain vertical scale. +# When noise is < -0.55 terrain is near-flat. +mgv5_np_factor (Factor noise) noise_params_2d 0, 1, (250, 250, 250), 920381, 3, 0.45, 2.0, eased + +# Y-level of average terrain surface. +mgv5_np_height (Height noise) noise_params_2d 0, 10, (250, 250, 250), 84174, 4, 0.5, 2.0, eased + +# First of two 3D noises that together define tunnels. +mgv5_np_cave1 (Cave1 noise) noise_params_3d 0, 12, (61, 61, 61), 52534, 3, 0.5, 2.0 + +# Second of two 3D noises that together define tunnels. +mgv5_np_cave2 (Cave2 noise) noise_params_3d 0, 12, (67, 67, 67), 10325, 3, 0.5, 2.0 + +# 3D noise defining giant caverns. +mgv5_np_cavern (Cavern noise) noise_params_3d 0, 1, (384, 128, 384), 723, 5, 0.63, 2.0 + +# 3D noise defining terrain. +mgv5_np_ground (Ground noise) noise_params_3d 0, 40, (80, 80, 80), 983240, 4, 0.55, 2.0, eased + +# 3D noise that determines number of dungeons per mapchunk. +mgv5_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), 0, 2, 0.8, 2.0 + +[*Mapgen V6] + +# Map generation attributes specific to Mapgen v6. +# The 'snowbiomes' flag enables the new 5 biome system. +# When the 'snowbiomes' flag is enabled jungles are automatically enabled and +# the 'jungles' flag is ignored. +mgv6_spflags (Mapgen V6 specific flags) flags jungles,biomeblend,mudflow,snowbiomes,noflat,trees jungles,biomeblend,mudflow,snowbiomes,flat,trees,nojungles,nobiomeblend,nomudflow,nosnowbiomes,noflat,notrees + +# Deserts occur when np_biome exceeds this value. +# When the 'snowbiomes' flag is enabled, this is ignored. +mgv6_freq_desert (Desert noise threshold) float 0.45 + +# Sandy beaches occur when np_beach exceeds this value. +mgv6_freq_beach (Beach noise threshold) float 0.15 + +# Lower Y limit of dungeons. +mgv6_dungeon_ymin (Dungeon minimum Y) int -31000 -31000 31000 + +# Upper Y limit of dungeons. +mgv6_dungeon_ymax (Dungeon maximum Y) int 31000 -31000 31000 + +[**Noises] + +# Y-level of lower terrain and seabed. +mgv6_np_terrain_base (Terrain base noise) noise_params_2d -4, 20, (250, 250, 250), 82341, 5, 0.6, 2.0, eased + +# Y-level of higher terrain that creates cliffs. +mgv6_np_terrain_higher (Terrain higher noise) noise_params_2d 20, 16, (500, 500, 500), 85039, 5, 0.6, 2.0, eased + +# Varies steepness of cliffs. +mgv6_np_steepness (Steepness noise) noise_params_2d 0.85, 0.5, (125, 125, 125), -932, 5, 0.7, 2.0, eased + +# Defines distribution of higher terrain. +mgv6_np_height_select (Height select noise) noise_params_2d 0.5, 1, (250, 250, 250), 4213, 5, 0.69, 2.0, eased + +# Varies depth of biome surface nodes. +mgv6_np_mud (Mud noise) noise_params_2d 4, 2, (200, 200, 200), 91013, 3, 0.55, 2.0, eased + +# Defines areas with sandy beaches. +mgv6_np_beach (Beach noise) noise_params_2d 0, 1, (250, 250, 250), 59420, 3, 0.50, 2.0, eased + +# Temperature variation for biomes. +mgv6_np_biome (Biome noise) noise_params_2d 0, 1, (500, 500, 500), 9130, 3, 0.50, 2.0, eased + +# Variation of number of caves. +mgv6_np_cave (Cave noise) noise_params_2d 6, 6, (250, 250, 250), 34329, 3, 0.50, 2.0, eased + +# Humidity variation for biomes. +mgv6_np_humidity (Humidity noise) noise_params_2d 0.5, 0.5, (500, 500, 500), 72384, 3, 0.50, 2.0, eased + +# Defines tree areas and tree density. +mgv6_np_trees (Trees noise) noise_params_2d 0, 1, (125, 125, 125), 2, 4, 0.66, 2.0, eased + +# Defines areas where trees have apples. +mgv6_np_apple_trees (Apple trees noise) noise_params_2d 0, 1, (100, 100, 100), 342902, 3, 0.45, 2.0, eased + +[*Mapgen V7] + +# Map generation attributes specific to Mapgen v7. +# 'ridges': Rivers. +# 'floatlands': Floating land masses in the atmosphere. +# 'caverns': Giant caves deep underground. +mgv7_spflags (Mapgen V7 specific flags) flags mountains,ridges,nofloatlands,caverns mountains,ridges,floatlands,caverns,nomountains,noridges,nofloatlands,nocaverns + +# Y of mountain density gradient zero level. Used to shift mountains vertically. +mgv7_mount_zero_level (Mountain zero level) int 0 -31000 31000 + +# Lower Y limit of floatlands. +mgv7_floatland_ymin (Floatland minimum Y) int 1024 -31000 31000 + +# Upper Y limit of floatlands. +mgv7_floatland_ymax (Floatland maximum Y) int 4096 -31000 31000 + +# Y-distance over which floatlands taper from full density to nothing. +# Tapering starts at this distance from the Y limit. +# For a solid floatland layer, this controls the height of hills/mountains. +# Must be less than or equal to half the distance between the Y limits. +mgv7_floatland_taper (Floatland tapering distance) int 256 0 32767 + +# Exponent of the floatland tapering. Alters the tapering behaviour. +# Value = 1.0 creates a uniform, linear tapering. +# Values > 1.0 create a smooth tapering suitable for the default separated +# floatlands. +# Values < 1.0 (for example 0.25) create a more defined surface level with +# flatter lowlands, suitable for a solid floatland layer. +mgv7_float_taper_exp (Floatland taper exponent) float 2.0 + +# Adjusts the density of the floatland layer. +# Increase value to increase density. Can be positive or negative. +# Value = 0.0: 50% of volume is floatland. +# Value = 2.0 (can be higher depending on 'mgv7_np_floatland', always test +# to be sure) creates a solid floatland layer. +mgv7_floatland_density (Floatland density) float -0.6 + +# Surface level of optional water placed on a solid floatland layer. +# Water is disabled by default and will only be placed if this value is set +# to above 'mgv7_floatland_ymax' - 'mgv7_floatland_taper' (the start of the +# upper tapering). +# ***WARNING, POTENTIAL DANGER TO WORLDS AND SERVER PERFORMANCE***: +# When enabling water placement the floatlands must be configured and tested +# to be a solid layer by setting 'mgv7_floatland_density' to 2.0 (or other +# required value depending on 'mgv7_np_floatland'), to avoid +# server-intensive extreme water flow and to avoid vast flooding of the +# world surface below. +mgv7_floatland_ywater (Floatland water level) int -31000 -31000 31000 + +# Controls width of tunnels, a smaller value creates wider tunnels. +# Value >= 10.0 completely disables generation of tunnels and avoids the +# intensive noise calculations. +mgv7_cave_width (Cave width) float 0.09 + +# Y of upper limit of large caves. +mgv7_large_cave_depth (Large cave depth) int -33 -31000 31000 + +# Minimum limit of random number of small caves per mapchunk. +mgv7_small_cave_num_min (Small cave minimum number) int 0 0 256 + +# Maximum limit of random number of small caves per mapchunk. +mgv7_small_cave_num_max (Small cave maximum number) int 0 0 256 + +# Minimum limit of random number of large caves per mapchunk. +mgv7_large_cave_num_min (Large cave minimum number) int 0 0 64 + +# Maximum limit of random number of large caves per mapchunk. +mgv7_large_cave_num_max (Large cave maximum number) int 2 0 64 + +# Proportion of large caves that contain liquid. +mgv7_large_cave_flooded (Large cave proportion flooded) float 0.5 0.0 1.0 + +# Y-level of cavern upper limit. +mgv7_cavern_limit (Cavern limit) int -256 -31000 31000 + +# Y-distance over which caverns expand to full size. +mgv7_cavern_taper (Cavern taper) int 256 0 32767 + +# Defines full size of caverns, smaller values create larger caverns. +mgv7_cavern_threshold (Cavern threshold) float 0.7 + +# Lower Y limit of dungeons. +mgv7_dungeon_ymin (Dungeon minimum Y) int -31000 -31000 31000 + +# Upper Y limit of dungeons. +mgv7_dungeon_ymax (Dungeon maximum Y) int 31000 -31000 31000 + +[**Noises] + +# Y-level of higher terrain that creates cliffs. +mgv7_np_terrain_base (Terrain base noise) noise_params_2d 4, 70, (600, 600, 600), 82341, 5, 0.6, 2.0, eased + +# Y-level of lower terrain and seabed. +mgv7_np_terrain_alt (Terrain alternative noise) noise_params_2d 4, 25, (600, 600, 600), 5934, 5, 0.6, 2.0, eased + +# Varies roughness of terrain. +# Defines the 'persistence' value for terrain_base and terrain_alt noises. +mgv7_np_terrain_persist (Terrain persistence noise) noise_params_2d 0.6, 0.1, (2000, 2000, 2000), 539, 3, 0.6, 2.0, eased + +# Defines distribution of higher terrain and steepness of cliffs. +mgv7_np_height_select (Height select noise) noise_params_2d -8, 16, (500, 500, 500), 4213, 6, 0.7, 2.0, eased + +# Variation of biome filler depth. +mgv7_np_filler_depth (Filler depth noise) noise_params_2d 0, 1.2, (150, 150, 150), 261, 3, 0.7, 2.0, eased + +# Variation of maximum mountain height (in nodes). +mgv7_np_mount_height (Mountain height noise) noise_params_2d 256, 112, (1000, 1000, 1000), 72449, 3, 0.6, 2.0, eased + +# Defines large-scale river channel structure. +mgv7_np_ridge_uwater (Ridge underwater noise) noise_params_2d 0, 1, (1000, 1000, 1000), 85039, 5, 0.6, 2.0, eased + +# 3D noise defining mountain structure and height. +# Also defines structure of floatland mountain terrain. +mgv7_np_mountain (Mountain noise) noise_params_3d -0.6, 1, (250, 350, 250), 5333, 5, 0.63, 2.0 + +# 3D noise defining structure of river canyon walls. +mgv7_np_ridge (Ridge noise) noise_params_3d 0, 1, (100, 100, 100), 6467, 4, 0.75, 2.0 + +# 3D noise defining structure of floatlands. +# If altered from the default, the noise 'scale' (0.7 by default) may need +# to be adjusted, as floatland tapering functions best when this noise has +# a value range of approximately -2.0 to 2.0. +mgv7_np_floatland (Floatland noise) noise_params_3d 0, 0.7, (384, 96, 384), 1009, 4, 0.75, 1.618 + +# 3D noise defining giant caverns. +mgv7_np_cavern (Cavern noise) noise_params_3d 0, 1, (384, 128, 384), 723, 5, 0.63, 2.0 + +# First of two 3D noises that together define tunnels. +mgv7_np_cave1 (Cave1 noise) noise_params_3d 0, 12, (61, 61, 61), 52534, 3, 0.5, 2.0 + +# Second of two 3D noises that together define tunnels. +mgv7_np_cave2 (Cave2 noise) noise_params_3d 0, 12, (67, 67, 67), 10325, 3, 0.5, 2.0 + +# 3D noise that determines number of dungeons per mapchunk. +mgv7_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), 0, 2, 0.8, 2.0 + +[*Mapgen Carpathian] + +# Map generation attributes specific to Mapgen Carpathian. +mgcarpathian_spflags (Mapgen Carpathian specific flags) flags caverns,norivers caverns,rivers,nocaverns,norivers + +# Defines the base ground level. +mgcarpathian_base_level (Base ground level) float 12.0 + +# Defines the width of the river channel. +mgcarpathian_river_width (River channel width) float 0.05 + +# Defines the depth of the river channel. +mgcarpathian_river_depth (River channel depth) float 24.0 + +# Defines the width of the river valley. +mgcarpathian_valley_width (River valley width) float 0.25 + +# Controls width of tunnels, a smaller value creates wider tunnels. +# Value >= 10.0 completely disables generation of tunnels and avoids the +# intensive noise calculations. +mgcarpathian_cave_width (Cave width) float 0.09 + +# Y of upper limit of large caves. +mgcarpathian_large_cave_depth (Large cave depth) int -33 -31000 31000 + +# Minimum limit of random number of small caves per mapchunk. +mgcarpathian_small_cave_num_min (Small cave minimum number) int 0 0 256 + +# Maximum limit of random number of small caves per mapchunk. +mgcarpathian_small_cave_num_max (Small cave maximum number) int 0 0 256 + +# Minimum limit of random number of large caves per mapchunk. +mgcarpathian_large_cave_num_min (Large cave minimum number) int 0 0 64 + +# Maximum limit of random number of large caves per mapchunk. +mgcarpathian_large_cave_num_max (Large cave maximum number) int 2 0 64 + +# Proportion of large caves that contain liquid. +mgcarpathian_large_cave_flooded (Large cave proportion flooded) float 0.5 0.0 1.0 + +# Y-level of cavern upper limit. +mgcarpathian_cavern_limit (Cavern limit) int -256 -31000 31000 + +# Y-distance over which caverns expand to full size. +mgcarpathian_cavern_taper (Cavern taper) int 256 0 32767 + +# Defines full size of caverns, smaller values create larger caverns. +mgcarpathian_cavern_threshold (Cavern threshold) float 0.7 + +# Lower Y limit of dungeons. +mgcarpathian_dungeon_ymin (Dungeon minimum Y) int -31000 -31000 31000 + +# Upper Y limit of dungeons. +mgcarpathian_dungeon_ymax (Dungeon maximum Y) int 31000 -31000 31000 + +[**Noises] + +# Variation of biome filler depth. +mgcarpathian_np_filler_depth (Filler depth noise) noise_params_2d 0, 1, (128, 128, 128), 261, 3, 0.7, 2.0, eased + +# First of 4 2D noises that together define hill/mountain range height. +mgcarpathian_np_height1 (Hilliness1 noise) noise_params_2d 0, 5, (251, 251, 251), 9613, 5, 0.5, 2.0, eased + +# Second of 4 2D noises that together define hill/mountain range height. +mgcarpathian_np_height2 (Hilliness2 noise) noise_params_2d 0, 5, (383, 383, 383), 1949, 5, 0.5, 2.0, eased + +# Third of 4 2D noises that together define hill/mountain range height. +mgcarpathian_np_height3 (Hilliness3 noise) noise_params_2d 0, 5, (509, 509, 509), 3211, 5, 0.5, 2.0, eased + +# Fourth of 4 2D noises that together define hill/mountain range height. +mgcarpathian_np_height4 (Hilliness4 noise) noise_params_2d 0, 5, (631, 631, 631), 1583, 5, 0.5, 2.0, eased + +# 2D noise that controls the size/occurrence of rolling hills. +mgcarpathian_np_hills_terrain (Rolling hills spread noise) noise_params_2d 1, 1, (1301, 1301, 1301), 1692, 3, 0.5, 2.0, eased + +# 2D noise that controls the size/occurrence of ridged mountain ranges. +mgcarpathian_np_ridge_terrain (Ridge mountain spread noise) noise_params_2d 1, 1, (1889, 1889, 1889), 3568, 3, 0.5, 2.0, eased + +# 2D noise that controls the size/occurrence of step mountain ranges. +mgcarpathian_np_step_terrain (Step mountain spread noise) noise_params_2d 1, 1, (1889, 1889, 1889), 4157, 3, 0.5, 2.0, eased + +# 2D noise that controls the shape/size of rolling hills. +mgcarpathian_np_hills (Rolling hill size noise) noise_params_2d 0, 3, (257, 257, 257), 6604, 6, 0.5, 2.0, eased + +# 2D noise that controls the shape/size of ridged mountains. +mgcarpathian_np_ridge_mnt (Ridged mountain size noise) noise_params_2d 0, 12, (743, 743, 743), 5520, 6, 0.7, 2.0, eased + +# 2D noise that controls the shape/size of step mountains. +mgcarpathian_np_step_mnt (Step mountain size noise) noise_params_2d 0, 8, (509, 509, 509), 2590, 6, 0.6, 2.0, eased + +# 2D noise that locates the river valleys and channels. +mgcarpathian_np_rivers (River noise) noise_params_2d 0, 1, (1000, 1000, 1000), 85039, 5, 0.6, 2.0, eased + +# 3D noise for mountain overhangs, cliffs, etc. Usually small variations. +mgcarpathian_np_mnt_var (Mountain variation noise) noise_params_3d 0, 1, (499, 499, 499), 2490, 5, 0.55, 2.0 + +# First of two 3D noises that together define tunnels. +mgcarpathian_np_cave1 (Cave1 noise) noise_params_3d 0, 12, (61, 61, 61), 52534, 3, 0.5, 2.0 + +# Second of two 3D noises that together define tunnels. +mgcarpathian_np_cave2 (Cave2 noise) noise_params_3d 0, 12, (67, 67, 67), 10325, 3, 0.5, 2.0 + +# 3D noise defining giant caverns. +mgcarpathian_np_cavern (Cavern noise) noise_params_3d 0, 1, (384, 128, 384), 723, 5, 0.63, 2.0 + +# 3D noise that determines number of dungeons per mapchunk. +mgcarpathian_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), 0, 2, 0.8, 2.0 + +[*Mapgen Flat] + +# Map generation attributes specific to Mapgen Flat. +# Occasional lakes and hills can be added to the flat world. +mgflat_spflags (Mapgen Flat specific flags) flags nolakes,nohills,nocaverns lakes,hills,caverns,nolakes,nohills,nocaverns + +# Y of flat ground. +mgflat_ground_level (Ground level) int 8 -31000 31000 + +# Y of upper limit of large caves. +mgflat_large_cave_depth (Large cave depth) int -33 -31000 31000 + +# Minimum limit of random number of small caves per mapchunk. +mgflat_small_cave_num_min (Small cave minimum number) int 0 0 256 + +# Maximum limit of random number of small caves per mapchunk. +mgflat_small_cave_num_max (Small cave maximum number) int 0 0 256 + +# Minimum limit of random number of large caves per mapchunk. +mgflat_large_cave_num_min (Large cave minimum number) int 0 0 64 + +# Maximum limit of random number of large caves per mapchunk. +mgflat_large_cave_num_max (Large cave maximum number) int 2 0 64 + +# Proportion of large caves that contain liquid. +mgflat_large_cave_flooded (Large cave proportion flooded) float 0.5 0.0 1.0 + +# Controls width of tunnels, a smaller value creates wider tunnels. +# Value >= 10.0 completely disables generation of tunnels and avoids the +# intensive noise calculations. +mgflat_cave_width (Cave width) float 0.09 + +# Terrain noise threshold for lakes. +# Controls proportion of world area covered by lakes. +# Adjust towards 0.0 for a larger proportion. +mgflat_lake_threshold (Lake threshold) float -0.45 + +# Controls steepness/depth of lake depressions. +mgflat_lake_steepness (Lake steepness) float 48.0 + +# Terrain noise threshold for hills. +# Controls proportion of world area covered by hills. +# Adjust towards 0.0 for a larger proportion. +mgflat_hill_threshold (Hill threshold) float 0.45 + +# Controls steepness/height of hills. +mgflat_hill_steepness (Hill steepness) float 64.0 + +# Y-level of cavern upper limit. +mgflat_cavern_limit (Cavern limit) int -256 -31000 31000 + +# Y-distance over which caverns expand to full size. +mgflat_cavern_taper (Cavern taper) int 256 0 32767 + +# Defines full size of caverns, smaller values create larger caverns. +mgflat_cavern_threshold (Cavern threshold) float 0.7 + +# Lower Y limit of dungeons. +mgflat_dungeon_ymin (Dungeon minimum Y) int -31000 -31000 31000 + +# Upper Y limit of dungeons. +mgflat_dungeon_ymax (Dungeon maximum Y) int 31000 -31000 31000 + +[**Noises] + +# Defines location and terrain of optional hills and lakes. +mgflat_np_terrain (Terrain noise) noise_params_2d 0, 1, (600, 600, 600), 7244, 5, 0.6, 2.0, eased + +# Variation of biome filler depth. +mgflat_np_filler_depth (Filler depth noise) noise_params_2d 0, 1.2, (150, 150, 150), 261, 3, 0.7, 2.0, eased + +# First of two 3D noises that together define tunnels. +mgflat_np_cave1 (Cave1 noise) noise_params_3d 0, 12, (61, 61, 61), 52534, 3, 0.5, 2.0 + +# Second of two 3D noises that together define tunnels. +mgflat_np_cave2 (Cave2 noise) noise_params_3d 0, 12, (67, 67, 67), 10325, 3, 0.5, 2.0 + +# 3D noise defining giant caverns. +mgflat_np_cavern (Cavern noise) noise_params_3d 0, 1, (384, 128, 384), 723, 5, 0.63, 2.0 + +# 3D noise that determines number of dungeons per mapchunk. +mgflat_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), 0, 2, 0.8, 2.0 + +[*Mapgen Fractal] + +# Map generation attributes specific to Mapgen Fractal. +# 'terrain' enables the generation of non-fractal terrain: +# ocean, islands and underground. +mgfractal_spflags (Mapgen Fractal specific flags) flags terrain terrain,noterrain + +# Controls width of tunnels, a smaller value creates wider tunnels. +# Value >= 10.0 completely disables generation of tunnels and avoids the +# intensive noise calculations. +mgfractal_cave_width (Cave width) float 0.09 + +# Y of upper limit of large caves. +mgfractal_large_cave_depth (Large cave depth) int -33 -31000 31000 + +# Minimum limit of random number of small caves per mapchunk. +mgfractal_small_cave_num_min (Small cave minimum number) int 0 0 256 + +# Maximum limit of random number of small caves per mapchunk. +mgfractal_small_cave_num_max (Small cave maximum number) int 0 0 256 + +# Minimum limit of random number of large caves per mapchunk. +mgfractal_large_cave_num_min (Large cave minimum number) int 0 0 64 + +# Maximum limit of random number of large caves per mapchunk. +mgfractal_large_cave_num_max (Large cave maximum number) int 2 0 64 + +# Proportion of large caves that contain liquid. +mgfractal_large_cave_flooded (Large cave proportion flooded) float 0.5 0.0 1.0 + +# Lower Y limit of dungeons. +mgfractal_dungeon_ymin (Dungeon minimum Y) int -31000 -31000 31000 + +# Upper Y limit of dungeons. +mgfractal_dungeon_ymax (Dungeon maximum Y) int 31000 -31000 31000 + +# Selects one of 18 fractal types. +# 1 = 4D "Roundy" Mandelbrot set. +# 2 = 4D "Roundy" Julia set. +# 3 = 4D "Squarry" Mandelbrot set. +# 4 = 4D "Squarry" Julia set. +# 5 = 4D "Mandy Cousin" Mandelbrot set. +# 6 = 4D "Mandy Cousin" Julia set. +# 7 = 4D "Variation" Mandelbrot set. +# 8 = 4D "Variation" Julia set. +# 9 = 3D "Mandelbrot/Mandelbar" Mandelbrot set. +# 10 = 3D "Mandelbrot/Mandelbar" Julia set. +# 11 = 3D "Christmas Tree" Mandelbrot set. +# 12 = 3D "Christmas Tree" Julia set. +# 13 = 3D "Mandelbulb" Mandelbrot set. +# 14 = 3D "Mandelbulb" Julia set. +# 15 = 3D "Cosine Mandelbulb" Mandelbrot set. +# 16 = 3D "Cosine Mandelbulb" Julia set. +# 17 = 4D "Mandelbulb" Mandelbrot set. +# 18 = 4D "Mandelbulb" Julia set. +mgfractal_fractal (Fractal type) int 1 1 18 + +# Iterations of the recursive function. +# Increasing this increases the amount of fine detail, but also +# increases processing load. +# At iterations = 20 this mapgen has a similar load to mapgen V7. +mgfractal_iterations (Iterations) int 11 1 65535 + +# (X,Y,Z) scale of fractal in nodes. +# Actual fractal size will be 2 to 3 times larger. +# These numbers can be made very large, the fractal does +# not have to fit inside the world. +# Increase these to 'zoom' into the detail of the fractal. +# Default is for a vertically-squashed shape suitable for +# an island, set all 3 numbers equal for the raw shape. +mgfractal_scale (Scale) v3f (4096.0, 1024.0, 4096.0) + +# (X,Y,Z) offset of fractal from world center in units of 'scale'. +# Can be used to move a desired point to (0, 0) to create a +# suitable spawn point, or to allow 'zooming in' on a desired +# point by increasing 'scale'. +# The default is tuned for a suitable spawn point for Mandelbrot +# sets with default parameters, it may need altering in other +# situations. +# Range roughly -2 to 2. Multiply by 'scale' for offset in nodes. +mgfractal_offset (Offset) v3f (1.79, 0.0, 0.0) + +# W coordinate of the generated 3D slice of a 4D fractal. +# Determines which 3D slice of the 4D shape is generated. +# Alters the shape of the fractal. +# Has no effect on 3D fractals. +# Range roughly -2 to 2. +mgfractal_slice_w (Slice w) float 0.0 + +# Julia set only. +# X component of hypercomplex constant. +# Alters the shape of the fractal. +# Range roughly -2 to 2. +mgfractal_julia_x (Julia x) float 0.33 + +# Julia set only. +# Y component of hypercomplex constant. +# Alters the shape of the fractal. +# Range roughly -2 to 2. +mgfractal_julia_y (Julia y) float 0.33 + +# Julia set only. +# Z component of hypercomplex constant. +# Alters the shape of the fractal. +# Range roughly -2 to 2. +mgfractal_julia_z (Julia z) float 0.33 + +# Julia set only. +# W component of hypercomplex constant. +# Alters the shape of the fractal. +# Has no effect on 3D fractals. +# Range roughly -2 to 2. +mgfractal_julia_w (Julia w) float 0.33 + +[**Noises] + +# Y-level of seabed. +mgfractal_np_seabed (Seabed noise) noise_params_2d -14, 9, (600, 600, 600), 41900, 5, 0.6, 2.0, eased + +# Variation of biome filler depth. +mgfractal_np_filler_depth (Filler depth noise) noise_params_2d 0, 1.2, (150, 150, 150), 261, 3, 0.7, 2.0, eased + +# First of two 3D noises that together define tunnels. +mgfractal_np_cave1 (Cave1 noise) noise_params_3d 0, 12, (61, 61, 61), 52534, 3, 0.5, 2.0 + +# Second of two 3D noises that together define tunnels. +mgfractal_np_cave2 (Cave2 noise) noise_params_3d 0, 12, (67, 67, 67), 10325, 3, 0.5, 2.0 + +# 3D noise that determines number of dungeons per mapchunk. +mgfractal_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), 0, 2, 0.8, 2.0 + +[*Mapgen Valleys] + +# Map generation attributes specific to Mapgen Valleys. +# 'altitude_chill': Reduces heat with altitude. +# 'humid_rivers': Increases humidity around rivers. +# 'vary_river_depth': If enabled, low humidity and high heat causes rivers +# to become shallower and occasionally dry. +# 'altitude_dry': Reduces humidity with altitude. +mgvalleys_spflags (Mapgen Valleys specific flags) flags altitude_chill,humid_rivers,vary_river_depth,altitude_dry altitude_chill,humid_rivers,vary_river_depth,altitude_dry,noaltitude_chill,nohumid_rivers,novary_river_depth,noaltitude_dry + +# The vertical distance over which heat drops by 20 if 'altitude_chill' is +# enabled. Also the vertical distance over which humidity drops by 10 if +# 'altitude_dry' is enabled. +mgvalleys_altitude_chill (Altitude chill) int 90 0 65535 + +# Depth below which you'll find large caves. +mgvalleys_large_cave_depth (Large cave depth) int -33 -31000 31000 + +# Minimum limit of random number of small caves per mapchunk. +mgvalleys_small_cave_num_min (Small cave minimum number) int 0 0 256 + +# Maximum limit of random number of small caves per mapchunk. +mgvalleys_small_cave_num_max (Small cave maximum number) int 0 0 256 + +# Minimum limit of random number of large caves per mapchunk. +mgvalleys_large_cave_num_min (Large cave minimum number) int 0 0 64 + +# Maximum limit of random number of large caves per mapchunk. +mgvalleys_large_cave_num_max (Large cave maximum number) int 2 0 64 + +# Proportion of large caves that contain liquid. +mgvalleys_large_cave_flooded (Large cave proportion flooded) float 0.5 0.0 1.0 + +# Depth below which you'll find giant caverns. +mgvalleys_cavern_limit (Cavern upper limit) int -256 -31000 31000 + +# Y-distance over which caverns expand to full size. +mgvalleys_cavern_taper (Cavern taper) int 192 0 32767 + +# Defines full size of caverns, smaller values create larger caverns. +mgvalleys_cavern_threshold (Cavern threshold) float 0.6 + +# How deep to make rivers. +mgvalleys_river_depth (River depth) int 4 0 65535 + +# How wide to make rivers. +mgvalleys_river_size (River size) int 5 0 65535 + +# Controls width of tunnels, a smaller value creates wider tunnels. +# Value >= 10.0 completely disables generation of tunnels and avoids the +# intensive noise calculations. +mgvalleys_cave_width (Cave width) float 0.09 + +# Lower Y limit of dungeons. +mgvalleys_dungeon_ymin (Dungeon minimum Y) int -31000 -31000 31000 + +# Upper Y limit of dungeons. +mgvalleys_dungeon_ymax (Dungeon maximum Y) int 63 -31000 31000 + +[**Noises] + +# First of two 3D noises that together define tunnels. +mgvalleys_np_cave1 (Cave noise #1) noise_params_3d 0, 12, (61, 61, 61), 52534, 3, 0.5, 2.0 + +# Second of two 3D noises that together define tunnels. +mgvalleys_np_cave2 (Cave noise #2) noise_params_3d 0, 12, (67, 67, 67), 10325, 3, 0.5, 2.0 + +# The depth of dirt or other biome filler node. +mgvalleys_np_filler_depth (Filler depth) noise_params_2d 0, 1.2, (256, 256, 256), 1605, 3, 0.5, 2.0, eased + +# 3D noise defining giant caverns. +mgvalleys_np_cavern (Cavern noise) noise_params_3d 0, 1, (768, 256, 768), 59033, 6, 0.63, 2.0 + +# Defines large-scale river channel structure. +mgvalleys_np_rivers (River noise) noise_params_2d 0, 1, (256, 256, 256), -6050, 5, 0.6, 2.0, eased + +# Base terrain height. +mgvalleys_np_terrain_height (Terrain height) noise_params_2d -10, 50, (1024, 1024, 1024), 5202, 6, 0.4, 2.0, eased + +# Raises terrain to make valleys around the rivers. +mgvalleys_np_valley_depth (Valley depth) noise_params_2d 5, 4, (512, 512, 512), -1914, 1, 1.0, 2.0, eased + +# Slope and fill work together to modify the heights. +mgvalleys_np_inter_valley_fill (Valley fill) noise_params_3d 0, 1, (256, 512, 256), 1993, 6, 0.8, 2.0 + +# Amplifies the valleys. +mgvalleys_np_valley_profile (Valley profile) noise_params_2d 0.6, 0.5, (512, 512, 512), 777, 1, 1.0, 2.0, eased + +# Slope and fill work together to modify the heights. +mgvalleys_np_inter_valley_slope (Valley slope) noise_params_2d 0.5, 0.5, (128, 128, 128), 746, 1, 1.0, 2.0, eased + +# 3D noise that determines number of dungeons per mapchunk. +mgvalleys_np_dungeons (Dungeon noise) noise_params_3d 0.9, 0.5, (500, 500, 500), 0, 2, 0.8, 2.0 + + +[Advanced] + +[*Developer Options] + +# Enable Lua modding support on client. +# This support is experimental and API can change. +enable_client_modding (Client modding) bool false + +# Replaces the default main menu with a custom one. +main_menu_script (Main menu script) string + +[**Mod Security] + +# Prevent mods from doing insecure things like running shell commands. +secure.enable_security (Enable mod security) bool true + +# Comma-separated list of trusted mods that are allowed to access insecure +# functions even when mod security is on (via request_insecure_environment()). +secure.trusted_mods (Trusted mods) string + +# Comma-separated list of mods that are allowed to access HTTP APIs, which +# allow them to upload and download data to/from the internet. +secure.http_mods (HTTP mods) string + +[**Debugging] + +# Level of logging to be written to debug.txt: +# - (no logging) +# - none (messages with no level) +# - error +# - warning +# - action +# - info +# - verbose +# - trace +debug_log_level (Debug log level) enum action ,none,error,warning,action,info,verbose,trace + +# If the file size of debug.txt exceeds the number of megabytes specified in +# this setting when it is opened, the file is moved to debug.txt.1, +# deleting an older debug.txt.1 if it exists. +# debug.txt is only moved if this setting is positive. +debug_log_size_max (Debug log file size threshold) int 50 1 + +# Minimal level of logging to be written to chat. +chat_log_level (Chat log level) enum error ,none,error,warning,action,info,verbose,trace + +# Handling for deprecated Lua API calls: +# - none: Do not log deprecated calls +# - log: mimic and log backtrace of deprecated call (default). +# - error: abort on usage of deprecated call (suggested for mod developers). +deprecated_lua_api_handling (Deprecated Lua API handling) enum log none,log,error + +# Enable random user input (only used for testing). +random_input (Random input) bool false + +# Enable mod channels support. +enable_mod_channels (Mod channels) bool false + +[**Mod Profiler] + +# Load the game profiler to collect game profiling data. +# Provides a /profiler command to access the compiled profile. +# Useful for mod developers and server operators. +profiler.load (Load the game profiler) bool false + +# The default format in which profiles are being saved, +# when calling `/profiler save [format]` without format. +profiler.default_report_format (Default report format) enum txt txt,csv,lua,json,json_pretty + +# The file path relative to your worldpath in which profiles will be saved to. +profiler.report_path (Report path) string "" + +# Instrument the methods of entities on registration. +instrument.entity (Entity methods) bool true + +# Instrument the action function of Active Block Modifiers on registration. +instrument.abm (Active Block Modifiers) bool true + +# Instrument the action function of Loading Block Modifiers on registration. +instrument.lbm (Loading Block Modifiers) bool true + +# Instrument chat commands on registration. +instrument.chatcommand (Chat commands) bool true + +# Instrument global callback functions on registration. +# (anything you pass to a minetest.register_*() function) +instrument.global_callback (Global callbacks) bool true + +# Instrument builtin. +# This is usually only needed by core/builtin contributors +instrument.builtin (Builtin) bool false + +# Have the profiler instrument itself: +# * Instrument an empty function. +# This estimates the overhead, that instrumentation is adding (+1 function call). +# * Instrument the sampler being used to update the statistics. +instrument.profiler (Profiler) bool false + +[**Engine profiler] + +# Print the engine's profiling data in regular intervals (in seconds). +# 0 = disable. Useful for developers. +profiler_print_interval (Engine profiling data print interval) int 0 0 + + +[*Advanced] + +# Enable IPv6 support (for both client and server). +# Required for IPv6 connections to work at all. +enable_ipv6 (IPv6) bool true + +# If enabled, invalid world data won't cause the server to shut down. +# Only enable this if you know what you are doing. +ignore_world_load_errors (Ignore world errors) bool false + +[**Graphics] + +# Path to shader directory. If no path is defined, default location will be used. +shader_path (Shader path) path + +# The rendering back-end. +# A restart is required after changing this. +# Note: On Android, stick with OGLES1 if unsure! App may fail to start otherwise. +# On other platforms, OpenGL is recommended. +# Shaders are supported by OpenGL (desktop only) and OGLES2 (experimental) +video_driver (Video driver) enum opengl opengl,ogles1,ogles2 + +# Distance in nodes at which transparency depth sorting is enabled +# Use this to limit the performance impact of transparency depth sorting +transparency_sorting_distance (Transparency Sorting Distance) int 16 0 128 + +# Enable vertex buffer objects. +# This should greatly improve graphics performance. +enable_vbo (VBO) bool true + +# Radius of cloud area stated in number of 64 node cloud squares. +# Values larger than 26 will start to produce sharp cutoffs at cloud area corners. +cloud_radius (Cloud radius) int 12 1 62 + +# Whether node texture animations should be desynchronized per mapblock. +desynchronize_mapblock_texture_animation (Desynchronize block animation) bool true + +# Enables caching of facedir rotated meshes. +enable_mesh_cache (Mesh cache) bool false + +# Delay between mesh updates on the client in ms. Increasing this will slow +# down the rate of mesh updates, thus reducing jitter on slower clients. +mesh_generation_interval (Mapblock mesh generation delay) int 0 0 50 + +# Size of the MapBlock cache of the mesh generator. Increasing this will +# increase the cache hit %, reducing the data being copied from the main +# thread, thus reducing jitter. +meshgen_block_cache_size (Mapblock mesh generator's MapBlock cache size in MB) int 20 0 1000 + +# True = 256 +# False = 128 +# Usable to make minimap smoother on slower machines. +minimap_double_scan_height (Minimap scan height) bool true + +# Textures on a node may be aligned either to the node or to the world. +# The former mode suits better things like machines, furniture, etc., while +# the latter makes stairs and microblocks fit surroundings better. +# However, as this possibility is new, thus may not be used by older servers, +# this option allows enforcing it for certain node types. Note though that +# that is considered EXPERIMENTAL and may not work properly. +world_aligned_mode (World-aligned textures mode) enum enable disable,enable,force_solid,force_nodebox + +# World-aligned textures may be scaled to span several nodes. However, +# the server may not send the scale you want, especially if you use +# a specially-designed texture pack; with this option, the client tries +# to determine the scale automatically basing on the texture size. +# See also texture_min_size. +# Warning: This option is EXPERIMENTAL! +autoscale_mode (Autoscaling mode) enum disable disable,enable,force + +[**Font] + +font_bold (Font bold by default) bool false + +font_italic (Font italic by default) bool false + +# Shadow offset (in pixels) of the default font. If 0, then shadow will not be drawn. +font_shadow (Font shadow) int 1 0 65535 + +# Opaqueness (alpha) of the shadow behind the default font, between 0 and 255. +font_shadow_alpha (Font shadow alpha) int 127 0 255 + +# Font size of the default font where 1 unit = 1 pixel at 96 DPI +font_size (Font size) int 16 5 72 + +# For pixel-style fonts that do not scale well, this ensures that font sizes used +# with this font will always be divisible by this value, in pixels. For instance, +# a pixel font 16 pixels tall should have this set to 16, so it will only ever be +# sized 16, 32, 48, etc., so a mod requesting a size of 25 will get 32. +font_size_divisible_by (Font size divisible by) int 1 1 + +# Path to the default font. Must be a TrueType font. +# The fallback font will be used if the font cannot be loaded. +font_path (Regular font path) filepath fonts/Arimo-Regular.ttf + +font_path_bold (Bold font path) filepath fonts/Arimo-Bold.ttf +font_path_italic (Italic font path) filepath fonts/Arimo-Italic.ttf +font_path_bold_italic (Bold and italic font path) filepath fonts/Arimo-BoldItalic.ttf + +# Font size of the monospace font where 1 unit = 1 pixel at 96 DPI +mono_font_size (Monospace font size) int 16 5 72 + +# For pixel-style fonts that do not scale well, this ensures that font sizes used +# with this font will always be divisible by this value, in pixels. For instance, +# a pixel font 16 pixels tall should have this set to 16, so it will only ever be +# sized 16, 32, 48, etc., so a mod requesting a size of 25 will get 32. +mono_font_size_divisible_by (Monospace font size divisible by) int 1 1 + +# Path to the monospace font. Must be a TrueType font. +# This font is used for e.g. the console and profiler screen. +mono_font_path (Monospace font path) filepath fonts/Cousine-Regular.ttf + +mono_font_path_bold (Bold monospace font path) filepath fonts/Cousine-Bold.ttf +mono_font_path_italic (Italic monospace font path) filepath fonts/Cousine-Italic.ttf +mono_font_path_bold_italic (Bold and italic monospace font path) filepath fonts/Cousine-BoldItalic.ttf + +# Path of the fallback font. Must be a TrueType font. +# This font will be used for certain languages or if the default font is unavailable. +fallback_font_path (Fallback font path) filepath fonts/DroidSansFallbackFull.ttf + +[**Lighting] + +# Gradient of light curve at minimum light level. +# Controls the contrast of the lowest light levels. +lighting_alpha (Light curve low gradient) float 0.0 0.0 3.0 + +# Gradient of light curve at maximum light level. +# Controls the contrast of the highest light levels. +lighting_beta (Light curve high gradient) float 1.5 0.0 3.0 + +# Strength of light curve boost. +# The 3 'boost' parameters define a range of the light +# curve that is boosted in brightness. +lighting_boost (Light curve boost) float 0.2 0.0 0.4 + +# Center of light curve boost range. +# Where 0.0 is minimum light level, 1.0 is maximum light level. +lighting_boost_center (Light curve boost center) float 0.5 0.0 1.0 + +# Spread of light curve boost range. +# Controls the width of the range to be boosted. +# Standard deviation of the light curve boost Gaussian. +lighting_boost_spread (Light curve boost spread) float 0.2 0.0 0.4 + +[**Networking] + +# Prometheus listener address. +# If Minetest is compiled with ENABLE_PROMETHEUS option enabled, +# enable metrics listener for Prometheus on that address. +# Metrics can be fetched on http://127.0.0.1:30000/metrics +prometheus_listener_address (Prometheus listener address) string 127.0.0.1:30000 + +# Maximum size of the out chat queue. +# 0 to disable queueing and -1 to make the queue size unlimited. +max_out_chat_queue_size (Maximum size of the out chat queue) int 20 -1 32767 + +# Timeout for client to remove unused map data from memory, in seconds. +client_unload_unused_data_timeout (Mapblock unload timeout) float 600.0 0.0 + +# Maximum number of mapblocks for client to be kept in memory. +# Set to -1 for unlimited amount. +client_mapblock_limit (Mapblock limit) int 7500 -1 2147483647 + +# Whether to show the client debug info (has the same effect as hitting F5). +show_debug (Show debug info) bool false + +# Maximum number of blocks that are simultaneously sent per client. +# The maximum total count is calculated dynamically: +# max_total = ceil((#clients + max_users) * per_client / 4) +max_simultaneous_block_sends_per_client (Maximum simultaneous block sends per client) int 40 1 4294967295 + +# To reduce lag, block transfers are slowed down when a player is building something. +# This determines how long they are slowed down after placing or removing a node. +full_block_send_enable_min_time_from_building (Delay in sending blocks after building) float 2.0 0.0 + +# Maximum number of packets sent per send step, if you have a slow connection +# try reducing it, but don't reduce it to a number below double of targeted +# client number. +max_packets_per_iteration (Max. packets per iteration) int 1024 1 65535 + +# Compression level to use when sending mapblocks to the client. +# -1 - use default compression level +# 0 - least compression, fastest +# 9 - best compression, slowest +map_compression_level_net (Map Compression Level for Network Transfer) int -1 -1 9 + +[**Server] + +# Format of player chat messages. The following strings are valid placeholders: +# @name, @message, @timestamp (optional) +chat_message_format (Chat message format) string <@name> @message + +# If the execution of a chat command takes longer than this specified time in +# seconds, add the time information to the chat command message +chatcommand_msg_time_threshold (Chat command time message threshold) float 0.1 0.0 + +# A message to be displayed to all clients when the server shuts down. +kick_msg_shutdown (Shutdown message) string Server shutting down. + +# A message to be displayed to all clients when the server crashes. +kick_msg_crash (Crash message) string This server has experienced an internal error. You will now be disconnected. + +# Whether to ask clients to reconnect after a (Lua) crash. +# Set this to true if your server is set up to restart automatically. +ask_reconnect_on_crash (Ask to reconnect after crash) bool false + +[**Server/Env Performance] + +# Length of a server tick and the interval at which objects are generally updated over +# network, stated in seconds. +dedicated_server_step (Dedicated server step) float 0.09 0.0 + +# Whether players are shown to clients without any range limit. +# Deprecated, use the setting player_transfer_distance instead. +unlimited_player_transfer_distance (Unlimited player transfer distance) bool true + +# Defines the maximal player transfer distance in blocks (0 = unlimited). +player_transfer_distance (Player transfer distance) int 0 0 65535 + +# From how far clients know about objects, stated in mapblocks (16 nodes). +# +# Setting this larger than active_block_range will also cause the server +# to maintain active objects up to this distance in the direction the +# player is looking. (This can avoid mobs suddenly disappearing from view) +active_object_send_range_blocks (Active object send range) int 8 1 65535 + +# The radius of the volume of blocks around every player that is subject to the +# active block stuff, stated in mapblocks (16 nodes). +# In active blocks objects are loaded and ABMs run. +# This is also the minimum range in which active objects (mobs) are maintained. +# This should be configured together with active_object_send_range_blocks. +active_block_range (Active block range) int 4 1 65535 + +# From how far blocks are sent to clients, stated in mapblocks (16 nodes). +max_block_send_distance (Max block send distance) int 12 1 65535 + +# Maximum number of forceloaded mapblocks. +max_forceloaded_blocks (Maximum forceloaded blocks) int 16 0 + +# Interval of sending time of day to clients, stated in seconds. +time_send_interval (Time send interval) float 5.0 0.001 + +# Interval of saving important changes in the world, stated in seconds. +server_map_save_interval (Map save interval) float 5.3 0.001 + +# How long the server will wait before unloading unused mapblocks, stated in seconds. +# Higher value is smoother, but will use more RAM. +server_unload_unused_data_timeout (Unload unused server data) int 29 0 4294967295 + +# Maximum number of statically stored objects in a block. +max_objects_per_block (Maximum objects per block) int 256 1 65535 + +# Length of time between active block management cycles, stated in seconds. +active_block_mgmt_interval (Active block management interval) float 2.0 0.0 + +# Length of time between Active Block Modifier (ABM) execution cycles, stated in seconds. +abm_interval (ABM interval) float 1.0 0.0 + +# The time budget allowed for ABMs to execute on each step +# (as a fraction of the ABM Interval) +abm_time_budget (ABM time budget) float 0.2 0.1 0.9 + +# Length of time between NodeTimer execution cycles, stated in seconds. +nodetimer_interval (NodeTimer interval) float 0.2 0.0 + +# Max liquids processed per step. +liquid_loop_max (Liquid loop max) int 100000 1 4294967295 + +# The time (in seconds) that the liquids queue may grow beyond processing +# capacity until an attempt is made to decrease its size by dumping old queue +# items. A value of 0 disables the functionality. +liquid_queue_purge_time (Liquid queue purge time) int 0 0 65535 + +# Liquid update interval in seconds. +liquid_update (Liquid update tick) float 1.0 0.001 + +# At this distance the server will aggressively optimize which blocks are sent to +# clients. +# Small values potentially improve performance a lot, at the expense of visible +# rendering glitches (some blocks will not be rendered under water and in caves, +# as well as sometimes on land). +# Setting this to a value greater than max_block_send_distance disables this +# optimization. +# Stated in mapblocks (16 nodes). +block_send_optimize_distance (Block send optimize distance) int 4 2 32767 + +# If enabled the server will perform map block occlusion culling based on +# on the eye position of the player. This can reduce the number of blocks +# sent to the client 50-80%. The client will not longer receive most invisible +# so that the utility of noclip mode is reduced. +server_side_occlusion_culling (Server side occlusion culling) bool true + +[**Mapgen] + +# Size of mapchunks generated by mapgen, stated in mapblocks (16 nodes). +# WARNING!: There is no benefit, and there are several dangers, in +# increasing this value above 5. +# Reducing this value increases cave and dungeon density. +# Altering this value is for special usage, leaving it unchanged is +# recommended. +chunksize (Chunk size) int 5 1 10 + +# Dump the mapgen debug information. +enable_mapgen_debug_info (Mapgen debug) bool false + +# Maximum number of blocks that can be queued for loading. +emergequeue_limit_total (Absolute limit of queued blocks to emerge) int 1024 1 1000000 + +# Maximum number of blocks to be queued that are to be loaded from file. +# This limit is enforced per player. +emergequeue_limit_diskonly (Per-player limit of queued blocks load from disk) int 128 1 1000000 + +# Maximum number of blocks to be queued that are to be generated. +# This limit is enforced per player. +emergequeue_limit_generate (Per-player limit of queued blocks to generate) int 128 1 1000000 + +# Number of emerge threads to use. +# Value 0: +# - Automatic selection. The number of emerge threads will be +# - 'number of processors - 2', with a lower limit of 1. +# Any other value: +# - Specifies the number of emerge threads, with a lower limit of 1. +# WARNING: Increasing the number of emerge threads increases engine mapgen +# speed, but this may harm game performance by interfering with other +# processes, especially in singleplayer and/or when running Lua code in +# 'on_generated'. For many users the optimum setting may be '1'. +num_emerge_threads (Number of emerge threads) int 1 0 32767 + +[**cURL] + +# Maximum time an interactive request (e.g. server list fetch) may take, stated in milliseconds. +curl_timeout (cURL interactive timeout) int 20000 100 2147483647 + +# Limits number of parallel HTTP requests. Affects: +# - Media fetch if server uses remote_media setting. +# - Serverlist download and server announcement. +# - Downloads performed by main menu (e.g. mod manager). +# Only has an effect if compiled with cURL. +curl_parallel_limit (cURL parallel limit) int 8 1 2147483647 + +# Maximum time a file download (e.g. a mod download) may take, stated in milliseconds. +curl_file_download_timeout (cURL file download timeout) int 300000 100 2147483647 + +[**Misc] + +# Adjust dpi configuration to your screen (non X11/Android only) e.g. for 4k screens. +screen_dpi (DPI) int 72 1 + +# Adjust the detected display density, used for scaling UI elements. +display_density_factor (Display Density Scaling Factor) float 1 0.5 5.0 + +# Windows systems only: Start Minetest with the command line window in the background. +# Contains the same information as the file debug.txt (default name). +enable_console (Enable console window) bool false + +# Number of extra blocks that can be loaded by /clearobjects at once. +# This is a trade-off between SQLite transaction overhead and +# memory consumption (4096=100MB, as a rule of thumb). +max_clearobjects_extra_loaded_blocks (Max. clearobjects extra blocks) int 4096 0 4294967295 + +# World directory (everything in the world is stored here). +# Not needed if starting from the main menu. +map-dir (Map directory) path + +# See https://www.sqlite.org/pragma.html#pragma_synchronous +sqlite_synchronous (Synchronous SQLite) enum 2 0,1,2 + +# Compression level to use when saving mapblocks to disk. +# -1 - use default compression level +# 0 - least compression, fastest +# 9 - best compression, slowest +map_compression_level_disk (Map Compression Level for Disk Storage) int -1 -1 9 + +# Enable usage of remote media server (if provided by server). +# Remote servers offer a significantly faster way to download media (e.g. textures) +# when connecting to the server. +enable_remote_media_server (Connect to external media server) bool true + +# File in client/serverlist/ that contains your favorite servers displayed in the +# Multiplayer Tab. +serverlist_file (Serverlist file) string favoriteservers.json + + +[*Gamepads] + +# Enable joysticks. Requires a restart to take effect +enable_joysticks (Enable joysticks) bool false + +# The identifier of the joystick to use +joystick_id (Joystick ID) int 0 0 255 + +# The type of joystick +joystick_type (Joystick type) enum auto auto,generic,xbox,dragonrise_gamecube + +# The time in seconds it takes between repeated events +# when holding down a joystick button combination. +repeat_joystick_button_time (Joystick button repetition interval) float 0.17 0.001 + +# The dead zone of the joystick +joystick_deadzone (Joystick dead zone) int 2048 0 65535 + +# The sensitivity of the joystick axes for moving the +# in-game view frustum around. +joystick_frustum_sensitivity (Joystick frustum sensitivity) float 170.0 0.001 + + +[*Temporary Settings] + +# Path to texture directory. All textures are first searched from here. +texture_path (Texture path) path + +# Enables minimap. +enable_minimap (Minimap) bool true + +# Shape of the minimap. Enabled = round, disabled = square. +minimap_shape_round (Round minimap) bool true + +# Address to connect to. +# Leave this blank to start a local server. +# Note that the address field in the main menu overrides this setting. +address (Server address) string + +# Port to connect to (UDP). +# Note that the port field in the main menu overrides this setting. +remote_port (Remote port) int 30000 1 65535 + +# Default game when creating a new world. +# This will be overridden when creating a world from the main menu. +default_game (Default game) string minetest + +# Enable players getting damage and dying. +enable_damage (Damage) bool false + +# Enable creative mode for all players +creative_mode (Creative) bool false + +# Whether to allow players to damage and kill each other. +enable_pvp (Player versus player) bool true + +# Player is able to fly without being affected by gravity. +# This requires the "fly" privilege on the server. +free_move (Flying) bool false + +# If enabled, makes move directions relative to the player's pitch when flying or swimming. +pitch_move (Pitch move mode) bool false + +# Fast movement (via the "Aux1" key). +# This requires the "fast" privilege on the server. +fast_move (Fast movement) bool false + +# If enabled together with fly mode, player is able to fly through solid nodes. +# This requires the "noclip" privilege on the server. +noclip (Noclip) bool false + +# Continuous forward movement, toggled by autoforward key. +# Press the autoforward key again or the backwards movement to disable. +continuous_forward (Continuous forward) bool false + +# Formspec default background opacity (between 0 and 255). +formspec_default_bg_opacity (Formspec Default Background Opacity) int 140 0 255 + +# Formspec default background color (R,G,B). +formspec_default_bg_color (Formspec Default Background Color) string (0,0,0) + +# Whether to show technical names. +# Affects mods and texture packs in the Content and Select Mods menus, as well as +# setting names in All Settings. +# Controlled by the checkbox in the "All settings" menu. +show_technical_names (Show technical names) bool false + +# Enables the sound system. +# If disabled, this completely disables all sounds everywhere and the in-game +# sound controls will be non-functional. +# Changing this setting requires a restart. +enable_sound (Sound) bool true + +# Key for moving the player forward. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_forward (Forward key) key KEY_KEY_W + +# Key for moving the player backward. +# Will also disable autoforward, when active. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_backward (Backward key) key KEY_KEY_S + +# Key for moving the player left. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_left (Left key) key KEY_KEY_A + +# Key for moving the player right. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_right (Right key) key KEY_KEY_D + +# Key for jumping. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_jump (Jump key) key KEY_SPACE + +# Key for sneaking. +# Also used for climbing down and descending in water if aux1_descends is disabled. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_sneak (Sneak key) key KEY_LSHIFT + +# Key for digging. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_dig (Dig key) key KEY_LBUTTON + +# Key for placing. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_place (Place key) key KEY_RBUTTON + +# Key for opening the inventory. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_inventory (Inventory key) key KEY_KEY_I + +# Key for moving fast in fast mode. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_aux1 (Aux1 key) key KEY_KEY_E + +# Key for opening the chat window. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_chat (Chat key) key KEY_KEY_T + +# Key for opening the chat window to type commands. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_cmd (Command key) key / + +# Key for opening the chat window to type local commands. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_cmd_local (Command key) key . + +# Key for toggling unlimited view range. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_rangeselect (Range select key) key KEY_KEY_R + +# Key for toggling flying. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_freemove (Fly key) key KEY_KEY_K + +# Key for toggling pitch move mode. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_pitchmove (Pitch move key) key KEY_KEY_P + +# Key for toggling fast mode. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_fastmove (Fast key) key KEY_KEY_J + +# Key for toggling noclip mode. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_noclip (Noclip key) key KEY_KEY_H + +# Key for selecting the next item in the hotbar. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_hotbar_next (Hotbar next key) key KEY_KEY_N + +# Key for selecting the previous item in the hotbar. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_hotbar_previous (Hotbar previous key) key KEY_KEY_B + +# Key for muting the game. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_mute (Mute key) key KEY_KEY_M + +# Key for increasing the volume. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_increase_volume (Inc. volume key) key + +# Key for decreasing the volume. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_decrease_volume (Dec. volume key) key + +# Key for toggling autoforward. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_autoforward (Automatic forward key) key + +# Key for toggling cinematic mode. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_cinematic (Cinematic mode key) key + +# Key for toggling display of minimap. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_minimap (Minimap key) key KEY_KEY_V + +# Key for taking screenshots. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_screenshot (Screenshot) key KEY_F12 + +# Key for dropping the currently selected item. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_drop (Drop item key) key KEY_KEY_Q + +# Key to use view zoom when possible. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_zoom (View zoom key) key KEY_KEY_Z + +# Key for selecting the first hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot1 (Hotbar slot 1 key) key KEY_KEY_1 + +# Key for selecting the second hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot2 (Hotbar slot 2 key) key KEY_KEY_2 + +# Key for selecting the third hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot3 (Hotbar slot 3 key) key KEY_KEY_3 + +# Key for selecting the fourth hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot4 (Hotbar slot 4 key) key KEY_KEY_4 + +# Key for selecting the fifth hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot5 (Hotbar slot 5 key) key KEY_KEY_5 + +# Key for selecting the sixth hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot6 (Hotbar slot 6 key) key KEY_KEY_6 + +# Key for selecting the seventh hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot7 (Hotbar slot 7 key) key KEY_KEY_7 + +# Key for selecting the eighth hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot8 (Hotbar slot 8 key) key KEY_KEY_8 + +# Key for selecting the ninth hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot9 (Hotbar slot 9 key) key KEY_KEY_9 + +# Key for selecting the tenth hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot10 (Hotbar slot 10 key) key KEY_KEY_0 + +# Key for selecting the 11th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot11 (Hotbar slot 11 key) key + +# Key for selecting the 12th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot12 (Hotbar slot 12 key) key + +# Key for selecting the 13th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot13 (Hotbar slot 13 key) key + +# Key for selecting the 14th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot14 (Hotbar slot 14 key) key + +# Key for selecting the 15th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot15 (Hotbar slot 15 key) key + +# Key for selecting the 16th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot16 (Hotbar slot 16 key) key + +# Key for selecting the 17th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot17 (Hotbar slot 17 key) key + +# Key for selecting the 18th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot18 (Hotbar slot 18 key) key + +# Key for selecting the 19th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot19 (Hotbar slot 19 key) key + +# Key for selecting the 20th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot20 (Hotbar slot 20 key) key + +# Key for selecting the 21st hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot21 (Hotbar slot 21 key) key + +# Key for selecting the 22nd hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot22 (Hotbar slot 22 key) key + +# Key for selecting the 23rd hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot23 (Hotbar slot 23 key) key + +# Key for selecting the 24th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot24 (Hotbar slot 24 key) key + +# Key for selecting the 25th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot25 (Hotbar slot 25 key) key + +# Key for selecting the 26th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot26 (Hotbar slot 26 key) key + +# Key for selecting the 27th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot27 (Hotbar slot 27 key) key + +# Key for selecting the 28th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot28 (Hotbar slot 28 key) key + +# Key for selecting the 29th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot29 (Hotbar slot 29 key) key + +# Key for selecting the 30th hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot30 (Hotbar slot 30 key) key + +# Key for selecting the 31st hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot31 (Hotbar slot 31 key) key + +# Key for selecting the 32nd hotbar slot. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_slot32 (Hotbar slot 32 key) key + +# Key for toggling the display of the HUD. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_toggle_hud (HUD toggle key) key KEY_F1 + +# Key for toggling the display of chat. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_toggle_chat (Chat toggle key) key KEY_F2 + +# Key for toggling the display of the large chat console. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_console (Large chat console key) key KEY_F10 + +# Key for toggling the display of fog. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_toggle_force_fog_off (Fog toggle key) key KEY_F3 + +# Key for toggling the camera update. Only used for development +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_toggle_update_camera (Camera update toggle key) key + +# Key for toggling the display of debug info. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_toggle_debug (Debug info toggle key) key KEY_F5 + +# Key for toggling the display of the profiler. Used for development. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_toggle_profiler (Profiler toggle key) key KEY_F6 + +# Key for switching between first- and third-person camera. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_camera_mode (Toggle camera mode key) key KEY_KEY_C + +# Key for increasing the viewing range. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_increase_viewing_range_min (View range increase key) key + + +# Key for decreasing the viewing range. +# See http://irrlicht.sourceforge.net/docu/namespaceirr.html#a54da2a0e231901735e3da1b0edf72eb3 +keymap_decrease_viewing_range_min (View range decrease key) key - diff --git a/client/serverlist/.gitignore b/client/serverlist/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/client/serverlist/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/client/shaders/3d_interlaced_merge/opengl_fragment.glsl b/client/shaders/3d_interlaced_merge/opengl_fragment.glsl new file mode 100644 index 0000000..6d3ae50 --- /dev/null +++ b/client/shaders/3d_interlaced_merge/opengl_fragment.glsl @@ -0,0 +1,23 @@ +uniform sampler2D baseTexture; +uniform sampler2D normalTexture; +uniform sampler2D textureFlags; + +#define leftImage baseTexture +#define rightImage normalTexture +#define maskImage textureFlags + +varying mediump vec4 varTexCoord; + +void main(void) +{ + vec2 uv = varTexCoord.st; + vec4 left = texture2D(leftImage, uv).rgba; + vec4 right = texture2D(rightImage, uv).rgba; + vec4 mask = texture2D(maskImage, uv).rgba; + vec4 color; + if (mask.r > 0.5) + color = right; + else + color = left; + gl_FragColor = color; +} diff --git a/client/shaders/3d_interlaced_merge/opengl_vertex.glsl b/client/shaders/3d_interlaced_merge/opengl_vertex.glsl new file mode 100644 index 0000000..224b7d1 --- /dev/null +++ b/client/shaders/3d_interlaced_merge/opengl_vertex.glsl @@ -0,0 +1,7 @@ +varying mediump vec4 varTexCoord; + +void main(void) +{ + varTexCoord = inTexCoord0; + gl_Position = inVertexPosition; +} diff --git a/client/shaders/default_shader/opengl_fragment.glsl b/client/shaders/default_shader/opengl_fragment.glsl new file mode 100644 index 0000000..5018ac6 --- /dev/null +++ b/client/shaders/default_shader/opengl_fragment.glsl @@ -0,0 +1,6 @@ +varying lowp vec4 varColor; + +void main(void) +{ + gl_FragColor = varColor; +} diff --git a/client/shaders/default_shader/opengl_vertex.glsl b/client/shaders/default_shader/opengl_vertex.glsl new file mode 100644 index 0000000..a908ac9 --- /dev/null +++ b/client/shaders/default_shader/opengl_vertex.glsl @@ -0,0 +1,11 @@ +varying lowp vec4 varColor; + +void main(void) +{ + gl_Position = mWorldViewProj * inVertexPosition; +#ifdef GL_ES + varColor = inVertexColor.bgra; +#else + varColor = inVertexColor; +#endif +} diff --git a/client/shaders/minimap_shader/opengl_fragment.glsl b/client/shaders/minimap_shader/opengl_fragment.glsl new file mode 100644 index 0000000..cef359e --- /dev/null +++ b/client/shaders/minimap_shader/opengl_fragment.glsl @@ -0,0 +1,35 @@ +uniform sampler2D baseTexture; +uniform sampler2D normalTexture; +uniform vec3 yawVec; + +varying lowp vec4 varColor; +varying mediump vec2 varTexCoord; + +void main (void) +{ + vec2 uv = varTexCoord.st; + + //texture sampling rate + const float step = 1.0 / 256.0; + float tl = texture2D(normalTexture, vec2(uv.x - step, uv.y + step)).r; + float t = texture2D(normalTexture, vec2(uv.x - step, uv.y - step)).r; + float tr = texture2D(normalTexture, vec2(uv.x + step, uv.y + step)).r; + float r = texture2D(normalTexture, vec2(uv.x + step, uv.y)).r; + float br = texture2D(normalTexture, vec2(uv.x + step, uv.y - step)).r; + float b = texture2D(normalTexture, vec2(uv.x, uv.y - step)).r; + float bl = texture2D(normalTexture, vec2(uv.x - step, uv.y - step)).r; + float l = texture2D(normalTexture, vec2(uv.x - step, uv.y)).r; + float dX = (tr + 2.0 * r + br) - (tl + 2.0 * l + bl); + float dY = (bl + 2.0 * b + br) - (tl + 2.0 * t + tr); + vec4 bump = vec4 (normalize(vec3 (dX, dY, 0.1)),1.0); + float height = 2.0 * texture2D(normalTexture, vec2(uv.x, uv.y)).r - 1.0; + vec4 base = texture2D(baseTexture, uv).rgba; + vec3 L = normalize(vec3(0.0, 0.75, 1.0)); + float specular = pow(clamp(dot(reflect(L, bump.xyz), yawVec), 0.0, 1.0), 1.0); + float diffuse = dot(yawVec, bump.xyz); + + vec3 color = (1.1 * diffuse + 0.05 * height + 0.5 * specular) * base.rgb; + vec4 col = vec4(color.rgb, base.a); + col *= varColor; + gl_FragColor = vec4(col.rgb, base.a); +} diff --git a/client/shaders/minimap_shader/opengl_vertex.glsl b/client/shaders/minimap_shader/opengl_vertex.glsl new file mode 100644 index 0000000..b23d271 --- /dev/null +++ b/client/shaders/minimap_shader/opengl_vertex.glsl @@ -0,0 +1,15 @@ +uniform mat4 mWorld; + +varying lowp vec4 varColor; +varying mediump vec2 varTexCoord; + +void main(void) +{ + varTexCoord = inTexCoord0.st; + gl_Position = mWorldViewProj * inVertexPosition; +#ifdef GL_ES + varColor = inVertexColor.bgra; +#else + varColor = inVertexColor; +#endif +} diff --git a/client/shaders/nodes_shader/opengl_fragment.glsl b/client/shaders/nodes_shader/opengl_fragment.glsl new file mode 100644 index 0000000..c4b947e --- /dev/null +++ b/client/shaders/nodes_shader/opengl_fragment.glsl @@ -0,0 +1,492 @@ +uniform sampler2D baseTexture; + +uniform vec3 dayLight; +uniform vec4 skyBgColor; +uniform float fogDistance; +uniform vec3 eyePosition; + +// The cameraOffset is the current center of the visible world. +uniform vec3 cameraOffset; +uniform float animationTimer; +#ifdef ENABLE_DYNAMIC_SHADOWS + // shadow texture + uniform sampler2D ShadowMapSampler; + // shadow uniforms + uniform vec3 v_LightDirection; + uniform float f_textureresolution; + uniform mat4 m_ShadowViewProj; + uniform float f_shadowfar; + uniform float f_shadow_strength; + uniform vec4 CameraPos; + uniform float xyPerspectiveBias0; + uniform float xyPerspectiveBias1; + + varying float adj_shadow_strength; + varying float cosLight; + varying float f_normal_length; + varying vec3 shadow_position; + varying float perspective_factor; +#endif + + +varying vec3 vNormal; +varying vec3 vPosition; +// World position in the visible world (i.e. relative to the cameraOffset.) +// This can be used for many shader effects without loss of precision. +// If the absolute position is required it can be calculated with +// cameraOffset + worldPosition (for large coordinates the limits of float +// precision must be considered). +varying vec3 worldPosition; +varying lowp vec4 varColor; +#ifdef GL_ES +varying mediump vec2 varTexCoord; +#else +centroid varying vec2 varTexCoord; +#endif +varying vec3 eyeVec; +varying float nightRatio; + +const float fogStart = FOG_START; +const float fogShadingParameter = 1.0 / ( 1.0 - fogStart); + +#ifdef ENABLE_DYNAMIC_SHADOWS + +// assuming near is always 1.0 +float getLinearDepth() +{ + return 2.0 * f_shadowfar / (f_shadowfar + 1.0 - (2.0 * gl_FragCoord.z - 1.0) * (f_shadowfar - 1.0)); +} + +vec3 getLightSpacePosition() +{ + return shadow_position * 0.5 + 0.5; +} +// custom smoothstep implementation because it's not defined in glsl1.2 +// https://docs.gl/sl4/smoothstep +float mtsmoothstep(in float edge0, in float edge1, in float x) +{ + float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); + return t * t * (3.0 - 2.0 * t); +} + +#ifdef COLORED_SHADOWS + +// c_precision of 128 fits within 7 base-10 digits +const float c_precision = 128.0; +const float c_precisionp1 = c_precision + 1.0; + +float packColor(vec3 color) +{ + return floor(color.b * c_precision + 0.5) + + floor(color.g * c_precision + 0.5) * c_precisionp1 + + floor(color.r * c_precision + 0.5) * c_precisionp1 * c_precisionp1; +} + +vec3 unpackColor(float value) +{ + vec3 color; + color.b = mod(value, c_precisionp1) / c_precision; + color.g = mod(floor(value / c_precisionp1), c_precisionp1) / c_precision; + color.r = floor(value / (c_precisionp1 * c_precisionp1)) / c_precision; + return color; +} + +vec4 getHardShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + vec4 texDepth = texture2D(shadowsampler, smTexCoord.xy).rgba; + + float visibility = step(0.0, realDistance - texDepth.r); + vec4 result = vec4(visibility, vec3(0.0,0.0,0.0));//unpackColor(texDepth.g)); + if (visibility < 0.1) { + visibility = step(0.0, realDistance - texDepth.b); + result = vec4(visibility, unpackColor(texDepth.a)); + } + return result; +} + +#else + +float getHardShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float texDepth = texture2D(shadowsampler, smTexCoord.xy).r; + float visibility = step(0.0, realDistance - texDepth); + return visibility; +} + +#endif + + +#if SHADOW_FILTER == 2 + #define PCFBOUND 2.0 // 5x5 + #define PCFSAMPLES 25 +#elif SHADOW_FILTER == 1 + #define PCFBOUND 1.0 // 3x3 + #define PCFSAMPLES 9 +#else + #define PCFBOUND 0.0 + #define PCFSAMPLES 1 +#endif + +#ifdef COLORED_SHADOWS +float getHardShadowDepth(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + vec4 texDepth = texture2D(shadowsampler, smTexCoord.xy); + float depth = max(realDistance - texDepth.r, realDistance - texDepth.b); + return depth; +} +#else +float getHardShadowDepth(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float texDepth = texture2D(shadowsampler, smTexCoord.xy).r; + float depth = realDistance - texDepth; + return depth; +} +#endif + +#define BASEFILTERRADIUS 1.0 + +float getPenumbraRadius(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + // Return fast if sharp shadows are requested + if (PCFBOUND == 0.0 || SOFTSHADOWRADIUS <= 0.0) + return 0.0; + + vec2 clampedpos; + float y, x; + float depth = getHardShadowDepth(shadowsampler, smTexCoord.xy, realDistance); + // A factor from 0 to 1 to reduce blurring of short shadows + float sharpness_factor = 1.0; + // conversion factor from shadow depth to blur radius + float depth_to_blur = f_shadowfar / SOFTSHADOWRADIUS / xyPerspectiveBias0; + if (depth > 0.0 && f_normal_length > 0.0) + // 5 is empirical factor that controls how fast shadow loses sharpness + sharpness_factor = clamp(5 * depth * depth_to_blur, 0.0, 1.0); + depth = 0.0; + + float world_to_texture = xyPerspectiveBias1 / perspective_factor / perspective_factor + * f_textureresolution / 2.0 / f_shadowfar; + float world_radius = 0.2; // shadow blur radius in world float coordinates, e.g. 0.2 = 0.02 of one node + + return max(BASEFILTERRADIUS * f_textureresolution / 4096.0, sharpness_factor * world_radius * world_to_texture * SOFTSHADOWRADIUS); +} + +#ifdef POISSON_FILTER +const vec2[64] poissonDisk = vec2[64]( + vec2(0.170019, -0.040254), + vec2(-0.299417, 0.791925), + vec2(0.645680, 0.493210), + vec2(-0.651784, 0.717887), + vec2(0.421003, 0.027070), + vec2(-0.817194, -0.271096), + vec2(-0.705374, -0.668203), + vec2(0.977050, -0.108615), + vec2(0.063326, 0.142369), + vec2(0.203528, 0.214331), + vec2(-0.667531, 0.326090), + vec2(-0.098422, -0.295755), + vec2(-0.885922, 0.215369), + vec2(0.566637, 0.605213), + vec2(0.039766, -0.396100), + vec2(0.751946, 0.453352), + vec2(0.078707, -0.715323), + vec2(-0.075838, -0.529344), + vec2(0.724479, -0.580798), + vec2(0.222999, -0.215125), + vec2(-0.467574, -0.405438), + vec2(-0.248268, -0.814753), + vec2(0.354411, -0.887570), + vec2(0.175817, 0.382366), + vec2(0.487472, -0.063082), + vec2(0.355476, 0.025357), + vec2(-0.084078, 0.898312), + vec2(0.488876, -0.783441), + vec2(0.470016, 0.217933), + vec2(-0.696890, -0.549791), + vec2(-0.149693, 0.605762), + vec2(0.034211, 0.979980), + vec2(0.503098, -0.308878), + vec2(-0.016205, -0.872921), + vec2(0.385784, -0.393902), + vec2(-0.146886, -0.859249), + vec2(0.643361, 0.164098), + vec2(0.634388, -0.049471), + vec2(-0.688894, 0.007843), + vec2(0.464034, -0.188818), + vec2(-0.440840, 0.137486), + vec2(0.364483, 0.511704), + vec2(0.034028, 0.325968), + vec2(0.099094, -0.308023), + vec2(0.693960, -0.366253), + vec2(0.678884, -0.204688), + vec2(0.001801, 0.780328), + vec2(0.145177, -0.898984), + vec2(0.062655, -0.611866), + vec2(0.315226, -0.604297), + vec2(-0.780145, 0.486251), + vec2(-0.371868, 0.882138), + vec2(0.200476, 0.494430), + vec2(-0.494552, -0.711051), + vec2(0.612476, 0.705252), + vec2(-0.578845, -0.768792), + vec2(-0.772454, -0.090976), + vec2(0.504440, 0.372295), + vec2(0.155736, 0.065157), + vec2(0.391522, 0.849605), + vec2(-0.620106, -0.328104), + vec2(0.789239, -0.419965), + vec2(-0.545396, 0.538133), + vec2(-0.178564, -0.596057) +); + +#ifdef COLORED_SHADOWS + +vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance); + if (radius < 0.1) { + // we are in the middle of even brightness, no need for filtering + return getHardShadowColor(shadowsampler, smTexCoord.xy, realDistance); + } + + vec2 clampedpos; + vec4 visibility = vec4(0.0); + float scale_factor = radius / f_textureresolution; + + int samples = (1 + 1 * int(SOFTSHADOWRADIUS > 1.0)) * PCFSAMPLES; // scale max samples for the soft shadows + samples = int(clamp(pow(4.0 * radius + 1.0, 2.0), 1.0, float(samples))); + int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples))); + int end_offset = int(samples) + init_offset; + + for (int x = init_offset; x < end_offset; x++) { + clampedpos = poissonDisk[x] * scale_factor + smTexCoord.xy; + visibility += getHardShadowColor(shadowsampler, clampedpos.xy, realDistance); + } + + return visibility / samples; +} + +#else + +float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance); + if (radius < 0.1) { + // we are in the middle of even brightness, no need for filtering + return getHardShadow(shadowsampler, smTexCoord.xy, realDistance); + } + + vec2 clampedpos; + float visibility = 0.0; + float scale_factor = radius / f_textureresolution; + + int samples = (1 + 1 * int(SOFTSHADOWRADIUS > 1.0)) * PCFSAMPLES; // scale max samples for the soft shadows + samples = int(clamp(pow(4.0 * radius + 1.0, 2.0), 1.0, float(samples))); + int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples))); + int end_offset = int(samples) + init_offset; + + for (int x = init_offset; x < end_offset; x++) { + clampedpos = poissonDisk[x] * scale_factor + smTexCoord.xy; + visibility += getHardShadow(shadowsampler, clampedpos.xy, realDistance); + } + + return visibility / samples; +} + +#endif + +#else +/* poisson filter disabled */ + +#ifdef COLORED_SHADOWS + +vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance); + if (radius < 0.1) { + // we are in the middle of even brightness, no need for filtering + return getHardShadowColor(shadowsampler, smTexCoord.xy, realDistance); + } + + vec2 clampedpos; + vec4 visibility = vec4(0.0); + float x, y; + float bound = (1 + 0.5 * int(SOFTSHADOWRADIUS > 1.0)) * PCFBOUND; // scale max bound for soft shadows + bound = clamp(0.5 * (4.0 * radius - 1.0), 0.5, bound); + float scale_factor = radius / bound / f_textureresolution; + float n = 0.0; + + // basic PCF filter + for (y = -bound; y <= bound; y += 1.0) + for (x = -bound; x <= bound; x += 1.0) { + clampedpos = vec2(x,y) * scale_factor + smTexCoord.xy; + visibility += getHardShadowColor(shadowsampler, clampedpos.xy, realDistance); + n += 1.0; + } + + return visibility / max(n, 1.0); +} + +#else +float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance); + if (radius < 0.1) { + // we are in the middle of even brightness, no need for filtering + return getHardShadow(shadowsampler, smTexCoord.xy, realDistance); + } + + vec2 clampedpos; + float visibility = 0.0; + float x, y; + float bound = (1 + 0.5 * int(SOFTSHADOWRADIUS > 1.0)) * PCFBOUND; // scale max bound for soft shadows + bound = clamp(0.5 * (4.0 * radius - 1.0), 0.5, bound); + float scale_factor = radius / bound / f_textureresolution; + float n = 0.0; + + // basic PCF filter + for (y = -bound; y <= bound; y += 1.0) + for (x = -bound; x <= bound; x += 1.0) { + clampedpos = vec2(x,y) * scale_factor + smTexCoord.xy; + visibility += getHardShadow(shadowsampler, clampedpos.xy, realDistance); + n += 1.0; + } + + return visibility / max(n, 1.0); +} + +#endif + +#endif +#endif + +#if ENABLE_TONE_MAPPING + +/* Hable's UC2 Tone mapping parameters + A = 0.22; + B = 0.30; + C = 0.10; + D = 0.20; + E = 0.01; + F = 0.30; + W = 11.2; + equation used: ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F +*/ + +vec3 uncharted2Tonemap(vec3 x) +{ + return ((x * (0.22 * x + 0.03) + 0.002) / (x * (0.22 * x + 0.3) + 0.06)) - 0.03333; +} + +vec4 applyToneMapping(vec4 color) +{ + color = vec4(pow(color.rgb, vec3(2.2)), color.a); + const float gamma = 1.6; + const float exposureBias = 5.5; + color.rgb = uncharted2Tonemap(exposureBias * color.rgb); + // Precalculated white_scale from + //vec3 whiteScale = 1.0 / uncharted2Tonemap(vec3(W)); + vec3 whiteScale = vec3(1.036015346); + color.rgb *= whiteScale; + return vec4(pow(color.rgb, vec3(1.0 / gamma)), color.a); +} +#endif + + + +void main(void) +{ + vec3 color; + vec2 uv = varTexCoord.st; + + vec4 base = texture2D(baseTexture, uv).rgba; + // If alpha is zero, we can just discard the pixel. This fixes transparency + // on GPUs like GC7000L, where GL_ALPHA_TEST is not implemented in mesa, + // and also on GLES 2, where GL_ALPHA_TEST is missing entirely. +#ifdef USE_DISCARD + if (base.a == 0.0) + discard; +#endif +#ifdef USE_DISCARD_REF + if (base.a < 0.5) + discard; +#endif + + color = base.rgb; + vec4 col = vec4(color.rgb * varColor.rgb, 1.0); + +#ifdef ENABLE_DYNAMIC_SHADOWS + if (f_shadow_strength > 0.0) { + float shadow_int = 0.0; + vec3 shadow_color = vec3(0.0, 0.0, 0.0); + vec3 posLightSpace = getLightSpacePosition(); + + float distance_rate = (1.0 - pow(clamp(2.0 * length(posLightSpace.xy - 0.5),0.0,1.0), 10.0)); + if (max(abs(posLightSpace.x - 0.5), abs(posLightSpace.y - 0.5)) > 0.5) + distance_rate = 0.0; + float f_adj_shadow_strength = max(adj_shadow_strength-mtsmoothstep(0.9,1.1, posLightSpace.z),0.0); + + if (distance_rate > 1e-7) { + +#ifdef COLORED_SHADOWS + vec4 visibility; + if (cosLight > 0.0 || f_normal_length < 1e-3) + visibility = getShadowColor(ShadowMapSampler, posLightSpace.xy, posLightSpace.z); + else + visibility = vec4(1.0, 0.0, 0.0, 0.0); + shadow_int = visibility.r; + shadow_color = visibility.gba; +#else + if (cosLight > 0.0 || f_normal_length < 1e-3) + shadow_int = getShadow(ShadowMapSampler, posLightSpace.xy, posLightSpace.z); + else + shadow_int = 1.0; +#endif + shadow_int *= distance_rate; + shadow_int = clamp(shadow_int, 0.0, 1.0); + + } + + // turns out that nightRatio falls off much faster than + // actual brightness of artificial light in relation to natual light. + // Power ratio was measured on torches in MTG (brightness = 14). + float adjusted_night_ratio = pow(max(0.0, nightRatio), 0.6); + + // Apply self-shadowing when light falls at a narrow angle to the surface + // Cosine of the cut-off angle. + const float self_shadow_cutoff_cosine = 0.035; + if (f_normal_length != 0 && cosLight < self_shadow_cutoff_cosine) { + shadow_int = max(shadow_int, 1 - clamp(cosLight, 0.0, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine); + shadow_color = mix(vec3(0.0), shadow_color, min(cosLight, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine); + } + + shadow_int *= f_adj_shadow_strength; + + // calculate fragment color from components: + col.rgb = + adjusted_night_ratio * col.rgb + // artificial light + (1.0 - adjusted_night_ratio) * ( // natural light + col.rgb * (1.0 - shadow_int * (1.0 - shadow_color)) + // filtered texture color + dayLight * shadow_color * shadow_int); // reflected filtered sunlight/moonlight + } +#endif + +#if ENABLE_TONE_MAPPING + col = applyToneMapping(col); +#endif + + // Due to a bug in some (older ?) graphics stacks (possibly in the glsl compiler ?), + // the fog will only be rendered correctly if the last operation before the + // clamp() is an addition. Else, the clamp() seems to be ignored. + // E.g. the following won't work: + // float clarity = clamp(fogShadingParameter + // * (fogDistance - length(eyeVec)) / fogDistance), 0.0, 1.0); + // As additions usually come for free following a multiplication, the new formula + // should be more efficient as well. + // Note: clarity = (1 - fogginess) + float clarity = clamp(fogShadingParameter + - fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0); + col = mix(skyBgColor, col, clarity); + col = vec4(col.rgb, base.a); + + gl_FragColor = col; +} diff --git a/client/shaders/nodes_shader/opengl_vertex.glsl b/client/shaders/nodes_shader/opengl_vertex.glsl new file mode 100644 index 0000000..d1fba28 --- /dev/null +++ b/client/shaders/nodes_shader/opengl_vertex.glsl @@ -0,0 +1,272 @@ +uniform mat4 mWorld; +// Color of the light emitted by the sun. +uniform vec3 dayLight; +uniform vec3 eyePosition; + +// The cameraOffset is the current center of the visible world. +uniform vec3 cameraOffset; +uniform float animationTimer; + +varying vec3 vNormal; +varying vec3 vPosition; +// World position in the visible world (i.e. relative to the cameraOffset.) +// This can be used for many shader effects without loss of precision. +// If the absolute position is required it can be calculated with +// cameraOffset + worldPosition (for large coordinates the limits of float +// precision must be considered). +varying vec3 worldPosition; +varying lowp vec4 varColor; +// The centroid keyword ensures that after interpolation the texture coordinates +// lie within the same bounds when MSAA is en- and disabled. +// This fixes the stripes problem with nearest-neighbour textures and MSAA. +#ifdef GL_ES +varying mediump vec2 varTexCoord; +#else +centroid varying vec2 varTexCoord; +#endif +#ifdef ENABLE_DYNAMIC_SHADOWS + // shadow uniforms + uniform vec3 v_LightDirection; + uniform float f_textureresolution; + uniform mat4 m_ShadowViewProj; + uniform float f_shadowfar; + uniform float f_shadow_strength; + uniform float f_timeofday; + uniform vec4 CameraPos; + + varying float cosLight; + varying float normalOffsetScale; + varying float adj_shadow_strength; + varying float f_normal_length; + varying vec3 shadow_position; + varying float perspective_factor; +#endif + + +varying vec3 eyeVec; +varying float nightRatio; +// Color of the light emitted by the light sources. +const vec3 artificialLight = vec3(1.04, 1.04, 1.04); +const float e = 2.718281828459; +const float BS = 10.0; +uniform float xyPerspectiveBias0; +uniform float xyPerspectiveBias1; +uniform float zPerspectiveBias; + +#ifdef ENABLE_DYNAMIC_SHADOWS + +vec4 getRelativePosition(in vec4 position) +{ + vec2 l = position.xy - CameraPos.xy; + vec2 s = l / abs(l); + s = (1.0 - s * CameraPos.xy); + l /= s; + return vec4(l, s); +} + +float getPerspectiveFactor(in vec4 relativePosition) +{ + float pDistance = length(relativePosition.xy); + float pFactor = pDistance * xyPerspectiveBias0 + xyPerspectiveBias1; + return pFactor; +} + +vec4 applyPerspectiveDistortion(in vec4 position) +{ + vec4 l = getRelativePosition(position); + float pFactor = getPerspectiveFactor(l); + l.xy /= pFactor; + position.xy = l.xy * l.zw + CameraPos.xy; + position.z *= zPerspectiveBias; + return position; +} + +// custom smoothstep implementation because it's not defined in glsl1.2 +// https://docs.gl/sl4/smoothstep +float mtsmoothstep(in float edge0, in float edge1, in float x) +{ + float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); + return t * t * (3.0 - 2.0 * t); +} +#endif + + +float smoothCurve(float x) +{ + return x * x * (3.0 - 2.0 * x); +} + + +float triangleWave(float x) +{ + return abs(fract(x + 0.5) * 2.0 - 1.0); +} + + +float smoothTriangleWave(float x) +{ + return smoothCurve(triangleWave(x)) * 2.0 - 1.0; +} + +// OpenGL < 4.3 does not support continued preprocessor lines +#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_TRANSPARENT || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_OPAQUE || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_BASIC) && ENABLE_WAVING_WATER + +// +// Simple, fast noise function. +// See: https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83 +// +vec4 perm(vec4 x) +{ + return mod(((x * 34.0) + 1.0) * x, 289.0); +} + +float snoise(vec3 p) +{ + vec3 a = floor(p); + vec3 d = p - a; + d = d * d * (3.0 - 2.0 * d); + + vec4 b = a.xxyy + vec4(0.0, 1.0, 0.0, 1.0); + vec4 k1 = perm(b.xyxy); + vec4 k2 = perm(k1.xyxy + b.zzww); + + vec4 c = k2 + a.zzzz; + vec4 k3 = perm(c); + vec4 k4 = perm(c + 1.0); + + vec4 o1 = fract(k3 * (1.0 / 41.0)); + vec4 o2 = fract(k4 * (1.0 / 41.0)); + + vec4 o3 = o2 * d.z + o1 * (1.0 - d.z); + vec2 o4 = o3.yw * d.x + o3.xz * (1.0 - d.x); + + return o4.y * d.y + o4.x * (1.0 - d.y); +} + +#endif + + + + +void main(void) +{ + varTexCoord = inTexCoord0.st; + + float disp_x; + float disp_z; +// OpenGL < 4.3 does not support continued preprocessor lines +#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_LEAVES && ENABLE_WAVING_LEAVES) || (MATERIAL_TYPE == TILE_MATERIAL_WAVING_PLANTS && ENABLE_WAVING_PLANTS) + vec4 pos2 = mWorld * inVertexPosition; + float tOffset = (pos2.x + pos2.y) * 0.001 + pos2.z * 0.002; + disp_x = (smoothTriangleWave(animationTimer * 23.0 + tOffset) + + smoothTriangleWave(animationTimer * 11.0 + tOffset)) * 0.4; + disp_z = (smoothTriangleWave(animationTimer * 31.0 + tOffset) + + smoothTriangleWave(animationTimer * 29.0 + tOffset) + + smoothTriangleWave(animationTimer * 13.0 + tOffset)) * 0.5; +#endif + + worldPosition = (mWorld * inVertexPosition).xyz; + +// OpenGL < 4.3 does not support continued preprocessor lines +#if (MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_TRANSPARENT || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_OPAQUE || MATERIAL_TYPE == TILE_MATERIAL_WAVING_LIQUID_BASIC) && ENABLE_WAVING_WATER + // Generate waves with Perlin-type noise. + // The constants are calibrated such that they roughly + // correspond to the old sine waves. + vec4 pos = inVertexPosition; + vec3 wavePos = worldPosition + cameraOffset; + // The waves are slightly compressed along the z-axis to get + // wave-fronts along the x-axis. + wavePos.x /= WATER_WAVE_LENGTH * 3.0; + wavePos.z /= WATER_WAVE_LENGTH * 2.0; + wavePos.z += animationTimer * WATER_WAVE_SPEED * 10.0; + pos.y += (snoise(wavePos) - 1.0) * WATER_WAVE_HEIGHT * 5.0; + gl_Position = mWorldViewProj * pos; +#elif MATERIAL_TYPE == TILE_MATERIAL_WAVING_LEAVES && ENABLE_WAVING_LEAVES + vec4 pos = inVertexPosition; + pos.x += disp_x; + pos.y += disp_z * 0.1; + pos.z += disp_z; + gl_Position = mWorldViewProj * pos; +#elif MATERIAL_TYPE == TILE_MATERIAL_WAVING_PLANTS && ENABLE_WAVING_PLANTS + vec4 pos = inVertexPosition; + if (varTexCoord.y < 0.05) { + pos.x += disp_x; + pos.z += disp_z; + } + gl_Position = mWorldViewProj * pos; +#else + gl_Position = mWorldViewProj * inVertexPosition; +#endif + + vPosition = gl_Position.xyz; + eyeVec = -(mWorldView * inVertexPosition).xyz; + vNormal = inVertexNormal; + + // Calculate color. + // Red, green and blue components are pre-multiplied with + // the brightness, so now we have to multiply these + // colors with the color of the incoming light. + // The pre-baked colors are halved to prevent overflow. +#ifdef GL_ES + vec4 color = inVertexColor.bgra; +#else + vec4 color = inVertexColor; +#endif + // The alpha gives the ratio of sunlight in the incoming light. + nightRatio = 1.0 - color.a; + color.rgb = color.rgb * (color.a * dayLight.rgb + + nightRatio * artificialLight.rgb) * 2.0; + color.a = 1.0; + + // Emphase blue a bit in darker places + // See C++ implementation in mapblock_mesh.cpp final_color_blend() + float brightness = (color.r + color.g + color.b) / 3.0; + color.b += max(0.0, 0.021 - abs(0.2 * brightness - 0.021) + + 0.07 * brightness); + + varColor = clamp(color, 0.0, 1.0); + +#ifdef ENABLE_DYNAMIC_SHADOWS + if (f_shadow_strength > 0.0) { + vec3 nNormal; + f_normal_length = length(vNormal); + + /* normalOffsetScale is in world coordinates (1/10th of a meter) + z_bias is in light space coordinates */ + float normalOffsetScale, z_bias; + float pFactor = getPerspectiveFactor(getRelativePosition(m_ShadowViewProj * mWorld * inVertexPosition)); + if (f_normal_length > 0.0) { + nNormal = normalize(vNormal); + cosLight = dot(nNormal, -v_LightDirection); + float sinLight = pow(1 - pow(cosLight, 2.0), 0.5); + normalOffsetScale = 2.0 * pFactor * pFactor * sinLight * min(f_shadowfar, 500.0) / + xyPerspectiveBias1 / f_textureresolution; + z_bias = 1.0 * sinLight / cosLight; + } + else { + nNormal = vec3(0.0); + cosLight = clamp(dot(v_LightDirection, normalize(vec3(v_LightDirection.x, 0.0, v_LightDirection.z))), 1e-2, 1.0); + float sinLight = pow(1 - pow(cosLight, 2.0), 0.5); + normalOffsetScale = 0.0; + z_bias = 3.6e3 * sinLight / cosLight; + } + z_bias *= pFactor * pFactor / f_textureresolution / f_shadowfar; + + shadow_position = applyPerspectiveDistortion(m_ShadowViewProj * mWorld * (inVertexPosition + vec4(normalOffsetScale * nNormal, 0.0))).xyz; + shadow_position.z -= z_bias; + perspective_factor = pFactor; + + if (f_timeofday < 0.2) { + adj_shadow_strength = f_shadow_strength * 0.5 * + (1.0 - mtsmoothstep(0.18, 0.2, f_timeofday)); + } else if (f_timeofday >= 0.8) { + adj_shadow_strength = f_shadow_strength * 0.5 * + mtsmoothstep(0.8, 0.83, f_timeofday); + } else { + adj_shadow_strength = f_shadow_strength * + mtsmoothstep(0.20, 0.25, f_timeofday) * + (1.0 - mtsmoothstep(0.7, 0.8, f_timeofday)); + } + } +#endif +} diff --git a/client/shaders/object_shader/opengl_fragment.glsl b/client/shaders/object_shader/opengl_fragment.glsl new file mode 100644 index 0000000..1fefc76 --- /dev/null +++ b/client/shaders/object_shader/opengl_fragment.glsl @@ -0,0 +1,495 @@ +uniform sampler2D baseTexture; + +uniform vec3 dayLight; +uniform vec4 skyBgColor; +uniform float fogDistance; +uniform vec3 eyePosition; + +// The cameraOffset is the current center of the visible world. +uniform vec3 cameraOffset; +uniform float animationTimer; +#ifdef ENABLE_DYNAMIC_SHADOWS + // shadow texture + uniform sampler2D ShadowMapSampler; + // shadow uniforms + uniform vec3 v_LightDirection; + uniform float f_textureresolution; + uniform mat4 m_ShadowViewProj; + uniform float f_shadowfar; + uniform float f_shadow_strength; + uniform vec4 CameraPos; + uniform float xyPerspectiveBias0; + uniform float xyPerspectiveBias1; + + varying float adj_shadow_strength; + varying float cosLight; + varying float f_normal_length; + varying vec3 shadow_position; + varying float perspective_factor; +#endif + + +varying vec3 vNormal; +varying vec3 vPosition; +// World position in the visible world (i.e. relative to the cameraOffset.) +// This can be used for many shader effects without loss of precision. +// If the absolute position is required it can be calculated with +// cameraOffset + worldPosition (for large coordinates the limits of float +// precision must be considered). +varying vec3 worldPosition; +varying lowp vec4 varColor; +#ifdef GL_ES +varying mediump vec2 varTexCoord; +#else +centroid varying vec2 varTexCoord; +#endif +varying vec3 eyeVec; +varying float nightRatio; + +varying float vIDiff; + +const float fogStart = FOG_START; +const float fogShadingParameter = 1.0 / (1.0 - fogStart); + +#ifdef ENABLE_DYNAMIC_SHADOWS + +// assuming near is always 1.0 +float getLinearDepth() +{ + return 2.0 * f_shadowfar / (f_shadowfar + 1.0 - (2.0 * gl_FragCoord.z - 1.0) * (f_shadowfar - 1.0)); +} + +vec3 getLightSpacePosition() +{ + return shadow_position * 0.5 + 0.5; +} +// custom smoothstep implementation because it's not defined in glsl1.2 +// https://docs.gl/sl4/smoothstep +float mtsmoothstep(in float edge0, in float edge1, in float x) +{ + float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); + return t * t * (3.0 - 2.0 * t); +} + +#ifdef COLORED_SHADOWS + +// c_precision of 128 fits within 7 base-10 digits +const float c_precision = 128.0; +const float c_precisionp1 = c_precision + 1.0; + +float packColor(vec3 color) +{ + return floor(color.b * c_precision + 0.5) + + floor(color.g * c_precision + 0.5) * c_precisionp1 + + floor(color.r * c_precision + 0.5) * c_precisionp1 * c_precisionp1; +} + +vec3 unpackColor(float value) +{ + vec3 color; + color.b = mod(value, c_precisionp1) / c_precision; + color.g = mod(floor(value / c_precisionp1), c_precisionp1) / c_precision; + color.r = floor(value / (c_precisionp1 * c_precisionp1)) / c_precision; + return color; +} + +vec4 getHardShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + vec4 texDepth = texture2D(shadowsampler, smTexCoord.xy).rgba; + + float visibility = step(0.0, realDistance - texDepth.r); + vec4 result = vec4(visibility, vec3(0.0,0.0,0.0));//unpackColor(texDepth.g)); + if (visibility < 0.1) { + visibility = step(0.0, realDistance - texDepth.b); + result = vec4(visibility, unpackColor(texDepth.a)); + } + return result; +} + +#else + +float getHardShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float texDepth = texture2D(shadowsampler, smTexCoord.xy).r; + float visibility = step(0.0, realDistance - texDepth); + return visibility; +} + +#endif + + +#if SHADOW_FILTER == 2 + #define PCFBOUND 2.0 // 5x5 + #define PCFSAMPLES 25 +#elif SHADOW_FILTER == 1 + #define PCFBOUND 1.0 // 3x3 + #define PCFSAMPLES 9 +#else + #define PCFBOUND 0.0 + #define PCFSAMPLES 1 +#endif + +#ifdef COLORED_SHADOWS +float getHardShadowDepth(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + vec4 texDepth = texture2D(shadowsampler, smTexCoord.xy); + float depth = max(realDistance - texDepth.r, realDistance - texDepth.b); + return depth; +} +#else +float getHardShadowDepth(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float texDepth = texture2D(shadowsampler, smTexCoord.xy).r; + float depth = realDistance - texDepth; + return depth; +} +#endif + +#define BASEFILTERRADIUS 1.0 + +float getPenumbraRadius(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + // Return fast if sharp shadows are requested + if (PCFBOUND == 0.0 || SOFTSHADOWRADIUS <= 0.0) + return 0.0; + + vec2 clampedpos; + float y, x; + float depth = getHardShadowDepth(shadowsampler, smTexCoord.xy, realDistance); + // A factor from 0 to 1 to reduce blurring of short shadows + float sharpness_factor = 1.0; + // conversion factor from shadow depth to blur radius + float depth_to_blur = f_shadowfar / SOFTSHADOWRADIUS / xyPerspectiveBias0; + if (depth > 0.0 && f_normal_length > 0.0) + // 5 is empirical factor that controls how fast shadow loses sharpness + sharpness_factor = clamp(5 * depth * depth_to_blur, 0.0, 1.0); + depth = 0.0; + + float world_to_texture = xyPerspectiveBias1 / perspective_factor / perspective_factor + * f_textureresolution / 2.0 / f_shadowfar; + float world_radius = 0.2; // shadow blur radius in world float coordinates, e.g. 0.2 = 0.02 of one node + + return max(BASEFILTERRADIUS * f_textureresolution / 4096.0, sharpness_factor * world_radius * world_to_texture * SOFTSHADOWRADIUS); +} + +#ifdef POISSON_FILTER +const vec2[64] poissonDisk = vec2[64]( + vec2(0.170019, -0.040254), + vec2(-0.299417, 0.791925), + vec2(0.645680, 0.493210), + vec2(-0.651784, 0.717887), + vec2(0.421003, 0.027070), + vec2(-0.817194, -0.271096), + vec2(-0.705374, -0.668203), + vec2(0.977050, -0.108615), + vec2(0.063326, 0.142369), + vec2(0.203528, 0.214331), + vec2(-0.667531, 0.326090), + vec2(-0.098422, -0.295755), + vec2(-0.885922, 0.215369), + vec2(0.566637, 0.605213), + vec2(0.039766, -0.396100), + vec2(0.751946, 0.453352), + vec2(0.078707, -0.715323), + vec2(-0.075838, -0.529344), + vec2(0.724479, -0.580798), + vec2(0.222999, -0.215125), + vec2(-0.467574, -0.405438), + vec2(-0.248268, -0.814753), + vec2(0.354411, -0.887570), + vec2(0.175817, 0.382366), + vec2(0.487472, -0.063082), + vec2(0.355476, 0.025357), + vec2(-0.084078, 0.898312), + vec2(0.488876, -0.783441), + vec2(0.470016, 0.217933), + vec2(-0.696890, -0.549791), + vec2(-0.149693, 0.605762), + vec2(0.034211, 0.979980), + vec2(0.503098, -0.308878), + vec2(-0.016205, -0.872921), + vec2(0.385784, -0.393902), + vec2(-0.146886, -0.859249), + vec2(0.643361, 0.164098), + vec2(0.634388, -0.049471), + vec2(-0.688894, 0.007843), + vec2(0.464034, -0.188818), + vec2(-0.440840, 0.137486), + vec2(0.364483, 0.511704), + vec2(0.034028, 0.325968), + vec2(0.099094, -0.308023), + vec2(0.693960, -0.366253), + vec2(0.678884, -0.204688), + vec2(0.001801, 0.780328), + vec2(0.145177, -0.898984), + vec2(0.062655, -0.611866), + vec2(0.315226, -0.604297), + vec2(-0.780145, 0.486251), + vec2(-0.371868, 0.882138), + vec2(0.200476, 0.494430), + vec2(-0.494552, -0.711051), + vec2(0.612476, 0.705252), + vec2(-0.578845, -0.768792), + vec2(-0.772454, -0.090976), + vec2(0.504440, 0.372295), + vec2(0.155736, 0.065157), + vec2(0.391522, 0.849605), + vec2(-0.620106, -0.328104), + vec2(0.789239, -0.419965), + vec2(-0.545396, 0.538133), + vec2(-0.178564, -0.596057) +); + +#ifdef COLORED_SHADOWS + +vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance); + if (radius < 0.1) { + // we are in the middle of even brightness, no need for filtering + return getHardShadowColor(shadowsampler, smTexCoord.xy, realDistance); + } + + vec2 clampedpos; + vec4 visibility = vec4(0.0); + float scale_factor = radius / f_textureresolution; + + int samples = (1 + 1 * int(SOFTSHADOWRADIUS > 1.0)) * PCFSAMPLES; // scale max samples for the soft shadows + samples = int(clamp(pow(4.0 * radius + 1.0, 2.0), 1.0, float(samples))); + int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples))); + int end_offset = int(samples) + init_offset; + + for (int x = init_offset; x < end_offset; x++) { + clampedpos = poissonDisk[x] * scale_factor + smTexCoord.xy; + visibility += getHardShadowColor(shadowsampler, clampedpos.xy, realDistance); + } + + return visibility / samples; +} + +#else + +float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance); + if (radius < 0.1) { + // we are in the middle of even brightness, no need for filtering + return getHardShadow(shadowsampler, smTexCoord.xy, realDistance); + } + + vec2 clampedpos; + float visibility = 0.0; + float scale_factor = radius / f_textureresolution; + + int samples = (1 + 1 * int(SOFTSHADOWRADIUS > 1.0)) * PCFSAMPLES; // scale max samples for the soft shadows + samples = int(clamp(pow(4.0 * radius + 1.0, 2.0), 1.0, float(samples))); + int init_offset = int(floor(mod(((smTexCoord.x * 34.0) + 1.0) * smTexCoord.y, 64.0-samples))); + int end_offset = int(samples) + init_offset; + + for (int x = init_offset; x < end_offset; x++) { + clampedpos = poissonDisk[x] * scale_factor + smTexCoord.xy; + visibility += getHardShadow(shadowsampler, clampedpos.xy, realDistance); + } + + return visibility / samples; +} + +#endif + +#else +/* poisson filter disabled */ + +#ifdef COLORED_SHADOWS + +vec4 getShadowColor(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance); + if (radius < 0.1) { + // we are in the middle of even brightness, no need for filtering + return getHardShadowColor(shadowsampler, smTexCoord.xy, realDistance); + } + + vec2 clampedpos; + vec4 visibility = vec4(0.0); + float x, y; + float bound = (1 + 0.5 * int(SOFTSHADOWRADIUS > 1.0)) * PCFBOUND; // scale max bound for soft shadows + bound = clamp(0.5 * (4.0 * radius - 1.0), 0.5, bound); + float scale_factor = radius / bound / f_textureresolution; + float n = 0.0; + + // basic PCF filter + for (y = -bound; y <= bound; y += 1.0) + for (x = -bound; x <= bound; x += 1.0) { + clampedpos = vec2(x,y) * scale_factor + smTexCoord.xy; + visibility += getHardShadowColor(shadowsampler, clampedpos.xy, realDistance); + n += 1.0; + } + + return visibility / max(n, 1.0); +} + +#else +float getShadow(sampler2D shadowsampler, vec2 smTexCoord, float realDistance) +{ + float radius = getPenumbraRadius(shadowsampler, smTexCoord, realDistance); + if (radius < 0.1) { + // we are in the middle of even brightness, no need for filtering + return getHardShadow(shadowsampler, smTexCoord.xy, realDistance); + } + + vec2 clampedpos; + float visibility = 0.0; + float x, y; + float bound = (1 + 0.5 * int(SOFTSHADOWRADIUS > 1.0)) * PCFBOUND; // scale max bound for soft shadows + bound = clamp(0.5 * (4.0 * radius - 1.0), 0.5, bound); + float scale_factor = radius / bound / f_textureresolution; + float n = 0.0; + + // basic PCF filter + for (y = -bound; y <= bound; y += 1.0) + for (x = -bound; x <= bound; x += 1.0) { + clampedpos = vec2(x,y) * scale_factor + smTexCoord.xy; + visibility += getHardShadow(shadowsampler, clampedpos.xy, realDistance); + n += 1.0; + } + + return visibility / max(n, 1.0); +} + +#endif + +#endif +#endif + +#if ENABLE_TONE_MAPPING + +/* Hable's UC2 Tone mapping parameters + A = 0.22; + B = 0.30; + C = 0.10; + D = 0.20; + E = 0.01; + F = 0.30; + W = 11.2; + equation used: ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F +*/ + +vec3 uncharted2Tonemap(vec3 x) +{ + return ((x * (0.22 * x + 0.03) + 0.002) / (x * (0.22 * x + 0.3) + 0.06)) - 0.03333; +} + +vec4 applyToneMapping(vec4 color) +{ + color = vec4(pow(color.rgb, vec3(2.2)), color.a); + const float gamma = 1.6; + const float exposureBias = 5.5; + color.rgb = uncharted2Tonemap(exposureBias * color.rgb); + // Precalculated white_scale from + //vec3 whiteScale = 1.0 / uncharted2Tonemap(vec3(W)); + vec3 whiteScale = vec3(1.036015346); + color.rgb *= whiteScale; + return vec4(pow(color.rgb, vec3(1.0 / gamma)), color.a); +} +#endif + + + +void main(void) +{ + vec3 color; + vec2 uv = varTexCoord.st; + + vec4 base = texture2D(baseTexture, uv).rgba; + // If alpha is zero, we can just discard the pixel. This fixes transparency + // on GPUs like GC7000L, where GL_ALPHA_TEST is not implemented in mesa, + // and also on GLES 2, where GL_ALPHA_TEST is missing entirely. +#ifdef USE_DISCARD + if (base.a == 0.0) + discard; +#endif +#ifdef USE_DISCARD_REF + if (base.a < 0.5) + discard; +#endif + + color = base.rgb; + vec4 col = vec4(color.rgb * varColor.rgb, 1.0); + col.rgb *= vIDiff; + +#ifdef ENABLE_DYNAMIC_SHADOWS + if (f_shadow_strength > 0.0) { + float shadow_int = 0.0; + vec3 shadow_color = vec3(0.0, 0.0, 0.0); + vec3 posLightSpace = getLightSpacePosition(); + + float distance_rate = (1.0 - pow(clamp(2.0 * length(posLightSpace.xy - 0.5),0.0,1.0), 10.0)); + if (max(abs(posLightSpace.x - 0.5), abs(posLightSpace.y - 0.5)) > 0.5) + distance_rate = 0.0; + float f_adj_shadow_strength = max(adj_shadow_strength-mtsmoothstep(0.9,1.1, posLightSpace.z),0.0); + + if (distance_rate > 1e-7) { + +#ifdef COLORED_SHADOWS + vec4 visibility; + if (cosLight > 0.0 || f_normal_length < 1e-3) + visibility = getShadowColor(ShadowMapSampler, posLightSpace.xy, posLightSpace.z); + else + visibility = vec4(1.0, 0.0, 0.0, 0.0); + shadow_int = visibility.r; + shadow_color = visibility.gba; +#else + if (cosLight > 0.0 || f_normal_length < 1e-3) + shadow_int = getShadow(ShadowMapSampler, posLightSpace.xy, posLightSpace.z); + else + shadow_int = 1.0; +#endif + shadow_int *= distance_rate; + shadow_int = clamp(shadow_int, 0.0, 1.0); + + } + + // turns out that nightRatio falls off much faster than + // actual brightness of artificial light in relation to natual light. + // Power ratio was measured on torches in MTG (brightness = 14). + float adjusted_night_ratio = pow(max(0.0, nightRatio), 0.6); + + // Apply self-shadowing when light falls at a narrow angle to the surface + // Cosine of the cut-off angle. + const float self_shadow_cutoff_cosine = 0.14; + if (f_normal_length != 0 && cosLight < self_shadow_cutoff_cosine) { + shadow_int = max(shadow_int, 1 - clamp(cosLight, 0.0, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine); + shadow_color = mix(vec3(0.0), shadow_color, min(cosLight, self_shadow_cutoff_cosine)/self_shadow_cutoff_cosine); + } + + shadow_int *= f_adj_shadow_strength; + + // calculate fragment color from components: + col.rgb = + adjusted_night_ratio * col.rgb + // artificial light + (1.0 - adjusted_night_ratio) * ( // natural light + col.rgb * (1.0 - shadow_int * (1.0 - shadow_color)) + // filtered texture color + dayLight * shadow_color * shadow_int); // reflected filtered sunlight/moonlight + } +#endif + +#if ENABLE_TONE_MAPPING + col = applyToneMapping(col); +#endif + + // Due to a bug in some (older ?) graphics stacks (possibly in the glsl compiler ?), + // the fog will only be rendered correctly if the last operation before the + // clamp() is an addition. Else, the clamp() seems to be ignored. + // E.g. the following won't work: + // float clarity = clamp(fogShadingParameter + // * (fogDistance - length(eyeVec)) / fogDistance), 0.0, 1.0); + // As additions usually come for free following a multiplication, the new formula + // should be more efficient as well. + // Note: clarity = (1 - fogginess) + float clarity = clamp(fogShadingParameter + - fogShadingParameter * length(eyeVec) / fogDistance, 0.0, 1.0); + col = mix(skyBgColor, col, clarity); + col = vec4(col.rgb, base.a); + + gl_FragColor = col; +} diff --git a/client/shaders/object_shader/opengl_vertex.glsl b/client/shaders/object_shader/opengl_vertex.glsl new file mode 100644 index 0000000..dc9c70c --- /dev/null +++ b/client/shaders/object_shader/opengl_vertex.glsl @@ -0,0 +1,181 @@ +uniform mat4 mWorld; +uniform vec3 dayLight; +uniform vec3 eyePosition; +uniform float animationTimer; +uniform vec4 emissiveColor; +uniform vec3 cameraOffset; + + +varying vec3 vNormal; +varying vec3 vPosition; +varying vec3 worldPosition; +varying lowp vec4 varColor; +#ifdef GL_ES +varying mediump vec2 varTexCoord; +#else +centroid varying vec2 varTexCoord; +#endif + +#ifdef ENABLE_DYNAMIC_SHADOWS + // shadow uniforms + uniform vec3 v_LightDirection; + uniform float f_textureresolution; + uniform mat4 m_ShadowViewProj; + uniform float f_shadowfar; + uniform float f_shadow_strength; + uniform float f_timeofday; + uniform vec4 CameraPos; + + varying float cosLight; + varying float adj_shadow_strength; + varying float f_normal_length; + varying vec3 shadow_position; + varying float perspective_factor; +#endif + +varying vec3 eyeVec; +varying float nightRatio; +// Color of the light emitted by the light sources. +const vec3 artificialLight = vec3(1.04, 1.04, 1.04); +varying float vIDiff; +const float e = 2.718281828459; +const float BS = 10.0; +uniform float xyPerspectiveBias0; +uniform float xyPerspectiveBias1; +uniform float zPerspectiveBias; + +#ifdef ENABLE_DYNAMIC_SHADOWS + +vec4 getRelativePosition(in vec4 position) +{ + vec2 l = position.xy - CameraPos.xy; + vec2 s = l / abs(l); + s = (1.0 - s * CameraPos.xy); + l /= s; + return vec4(l, s); +} + +float getPerspectiveFactor(in vec4 relativePosition) +{ + float pDistance = length(relativePosition.xy); + float pFactor = pDistance * xyPerspectiveBias0 + xyPerspectiveBias1; + return pFactor; +} + +vec4 applyPerspectiveDistortion(in vec4 position) +{ + vec4 l = getRelativePosition(position); + float pFactor = getPerspectiveFactor(l); + l.xy /= pFactor; + position.xy = l.xy * l.zw + CameraPos.xy; + position.z *= zPerspectiveBias; + return position; +} + +// custom smoothstep implementation because it's not defined in glsl1.2 +// https://docs.gl/sl4/smoothstep +float mtsmoothstep(in float edge0, in float edge1, in float x) +{ + float t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); + return t * t * (3.0 - 2.0 * t); +} +#endif + + +float directional_ambient(vec3 normal) +{ + vec3 v = normal * normal; + + if (normal.y < 0.0) + return dot(v, vec3(0.670820, 0.447213, 0.836660)); + + return dot(v, vec3(0.670820, 1.000000, 0.836660)); +} + +void main(void) +{ + varTexCoord = (mTexture * inTexCoord0).st; + gl_Position = mWorldViewProj * inVertexPosition; + + vPosition = gl_Position.xyz; + vNormal = (mWorld * vec4(inVertexNormal, 0.0)).xyz; + worldPosition = (mWorld * inVertexPosition).xyz; + eyeVec = -(mWorldView * inVertexPosition).xyz; + +#if (MATERIAL_TYPE == TILE_MATERIAL_PLAIN) || (MATERIAL_TYPE == TILE_MATERIAL_PLAIN_ALPHA) + vIDiff = 1.0; +#else + // This is intentional comparison with zero without any margin. + // If normal is not equal to zero exactly, then we assume it's a valid, just not normalized vector + vIDiff = length(inVertexNormal) == 0.0 + ? 1.0 + : directional_ambient(normalize(inVertexNormal)); +#endif + +#ifdef GL_ES + vec4 color = inVertexColor.bgra; +#else + vec4 color = inVertexColor; +#endif + + color *= emissiveColor; + + // The alpha gives the ratio of sunlight in the incoming light. + nightRatio = 1.0 - color.a; + color.rgb = color.rgb * (color.a * dayLight.rgb + + nightRatio * artificialLight.rgb) * 2.0; + color.a = 1.0; + + // Emphase blue a bit in darker places + // See C++ implementation in mapblock_mesh.cpp final_color_blend() + float brightness = (color.r + color.g + color.b) / 3.0; + color.b += max(0.0, 0.021 - abs(0.2 * brightness - 0.021) + + 0.07 * brightness); + + varColor = clamp(color, 0.0, 1.0); + + +#ifdef ENABLE_DYNAMIC_SHADOWS + if (f_shadow_strength > 0.0) { + vec3 nNormal = normalize(vNormal); + f_normal_length = length(vNormal); + + /* normalOffsetScale is in world coordinates (1/10th of a meter) + z_bias is in light space coordinates */ + float normalOffsetScale, z_bias; + float pFactor = getPerspectiveFactor(getRelativePosition(m_ShadowViewProj * mWorld * inVertexPosition)); + if (f_normal_length > 0.0) { + nNormal = normalize(vNormal); + cosLight = dot(nNormal, -v_LightDirection); + float sinLight = pow(1 - pow(cosLight, 2.0), 0.5); + normalOffsetScale = 0.1 * pFactor * pFactor * sinLight * min(f_shadowfar, 500.0) / + xyPerspectiveBias1 / f_textureresolution; + z_bias = 1e3 * sinLight / cosLight * (0.5 + f_textureresolution / 1024.0); + } + else { + nNormal = vec3(0.0); + cosLight = clamp(dot(v_LightDirection, normalize(vec3(v_LightDirection.x, 0.0, v_LightDirection.z))), 1e-2, 1.0); + float sinLight = pow(1 - pow(cosLight, 2.0), 0.5); + normalOffsetScale = 0.0; + z_bias = 3.6e3 * sinLight / cosLight; + } + z_bias *= pFactor * pFactor / f_textureresolution / f_shadowfar; + + shadow_position = applyPerspectiveDistortion(m_ShadowViewProj * mWorld * (inVertexPosition + vec4(normalOffsetScale * nNormal, 0.0))).xyz; + shadow_position.z -= z_bias; + perspective_factor = pFactor; + + if (f_timeofday < 0.2) { + adj_shadow_strength = f_shadow_strength * 0.5 * + (1.0 - mtsmoothstep(0.18, 0.2, f_timeofday)); + } else if (f_timeofday >= 0.8) { + adj_shadow_strength = f_shadow_strength * 0.5 * + mtsmoothstep(0.8, 0.83, f_timeofday); + } else { + adj_shadow_strength = f_shadow_strength * + mtsmoothstep(0.20, 0.25, f_timeofday) * + (1.0 - mtsmoothstep(0.7, 0.8, f_timeofday)); + } + } +#endif +} diff --git a/client/shaders/selection_shader/opengl_fragment.glsl b/client/shaders/selection_shader/opengl_fragment.glsl new file mode 100644 index 0000000..35b1f89 --- /dev/null +++ b/client/shaders/selection_shader/opengl_fragment.glsl @@ -0,0 +1,12 @@ +uniform sampler2D baseTexture; + +varying lowp vec4 varColor; +varying mediump vec2 varTexCoord; + +void main(void) +{ + vec2 uv = varTexCoord.st; + vec4 color = texture2D(baseTexture, uv); + color.rgb *= varColor.rgb; + gl_FragColor = color; +} diff --git a/client/shaders/selection_shader/opengl_vertex.glsl b/client/shaders/selection_shader/opengl_vertex.glsl new file mode 100644 index 0000000..39dde30 --- /dev/null +++ b/client/shaders/selection_shader/opengl_vertex.glsl @@ -0,0 +1,14 @@ +varying lowp vec4 varColor; +varying mediump vec2 varTexCoord; + +void main(void) +{ + varTexCoord = inTexCoord0.st; + gl_Position = mWorldViewProj * inVertexPosition; + +#ifdef GL_ES + varColor = inVertexColor.bgra; +#else + varColor = inVertexColor; +#endif +} diff --git a/client/shaders/shadow_shaders/pass1_fragment.glsl b/client/shaders/shadow_shaders/pass1_fragment.glsl new file mode 100644 index 0000000..2105def --- /dev/null +++ b/client/shaders/shadow_shaders/pass1_fragment.glsl @@ -0,0 +1,13 @@ +uniform sampler2D ColorMapSampler; +varying vec4 tPos; + +void main() +{ + vec4 col = texture2D(ColorMapSampler, gl_TexCoord[0].st); + + if (col.a < 0.70) + discard; + + float depth = 0.5 + tPos.z * 0.5; + gl_FragColor = vec4(depth, 0.0, 0.0, 1.0); +} diff --git a/client/shaders/shadow_shaders/pass1_trans_fragment.glsl b/client/shaders/shadow_shaders/pass1_trans_fragment.glsl new file mode 100644 index 0000000..b267c22 --- /dev/null +++ b/client/shaders/shadow_shaders/pass1_trans_fragment.glsl @@ -0,0 +1,42 @@ +uniform sampler2D ColorMapSampler; +varying vec4 tPos; + +#ifdef COLORED_SHADOWS +varying vec3 varColor; + +// c_precision of 128 fits within 7 base-10 digits +const float c_precision = 128.0; +const float c_precisionp1 = c_precision + 1.0; + +float packColor(vec3 color) +{ + return floor(color.b * c_precision + 0.5) + + floor(color.g * c_precision + 0.5) * c_precisionp1 + + floor(color.r * c_precision + 0.5) * c_precisionp1 * c_precisionp1; +} + +const vec3 black = vec3(0.0); +#endif + +void main() +{ + vec4 col = texture2D(ColorMapSampler, gl_TexCoord[0].st); +#ifndef COLORED_SHADOWS + if (col.a < 0.5) + discard; +#endif + + float depth = 0.5 + tPos.z * 0.5; + // ToDo: Liso: Apply movement on waving plants + // depth in [0, 1] for texture + + //col.rgb = col.a == 1.0 ? vec3(1.0) : col.rgb; +#ifdef COLORED_SHADOWS + col.rgb *= varColor.rgb; + // premultiply color alpha (see-through side) + float packedColor = packColor(col.rgb * (1.0 - col.a)); + gl_FragColor = vec4(depth, packedColor, 0.0,1.0); +#else + gl_FragColor = vec4(depth, 0.0, 0.0, 1.0); +#endif +} diff --git a/client/shaders/shadow_shaders/pass1_trans_vertex.glsl b/client/shaders/shadow_shaders/pass1_trans_vertex.glsl new file mode 100644 index 0000000..244d256 --- /dev/null +++ b/client/shaders/shadow_shaders/pass1_trans_vertex.glsl @@ -0,0 +1,50 @@ +uniform mat4 LightMVP; // world matrix +uniform vec4 CameraPos; +varying vec4 tPos; +#ifdef COLORED_SHADOWS +varying vec3 varColor; +#endif + +uniform float xyPerspectiveBias0; +uniform float xyPerspectiveBias1; +uniform float zPerspectiveBias; + +vec4 getRelativePosition(in vec4 position) +{ + vec2 l = position.xy - CameraPos.xy; + vec2 s = l / abs(l); + s = (1.0 - s * CameraPos.xy); + l /= s; + return vec4(l, s); +} + +float getPerspectiveFactor(in vec4 relativePosition) +{ + float pDistance = length(relativePosition.xy); + float pFactor = pDistance * xyPerspectiveBias0 + xyPerspectiveBias1; + return pFactor; +} + +vec4 applyPerspectiveDistortion(in vec4 position) +{ + vec4 l = getRelativePosition(position); + float pFactor = getPerspectiveFactor(l); + l.xy /= pFactor; + position.xy = l.xy * l.zw + CameraPos.xy; + position.z *= zPerspectiveBias; + return position; +} + +void main() +{ + vec4 pos = LightMVP * gl_Vertex; + + tPos = applyPerspectiveDistortion(LightMVP * gl_Vertex); + + gl_Position = vec4(tPos.xyz, 1.0); + gl_TexCoord[0].st = gl_MultiTexCoord0.st; + +#ifdef COLORED_SHADOWS + varColor = gl_Color.rgb; +#endif +} diff --git a/client/shaders/shadow_shaders/pass1_vertex.glsl b/client/shaders/shadow_shaders/pass1_vertex.glsl new file mode 100644 index 0000000..1dceb93 --- /dev/null +++ b/client/shaders/shadow_shaders/pass1_vertex.glsl @@ -0,0 +1,43 @@ +uniform mat4 LightMVP; // world matrix +uniform vec4 CameraPos; // camera position +varying vec4 tPos; + +uniform float xyPerspectiveBias0; +uniform float xyPerspectiveBias1; +uniform float zPerspectiveBias; + +vec4 getRelativePosition(in vec4 position) +{ + vec2 l = position.xy - CameraPos.xy; + vec2 s = l / abs(l); + s = (1.0 - s * CameraPos.xy); + l /= s; + return vec4(l, s); +} + +float getPerspectiveFactor(in vec4 relativePosition) +{ + float pDistance = length(relativePosition.xy); + float pFactor = pDistance * xyPerspectiveBias0 + xyPerspectiveBias1; + return pFactor; +} + +vec4 applyPerspectiveDistortion(in vec4 position) +{ + vec4 l = getRelativePosition(position); + float pFactor = getPerspectiveFactor(l); + l.xy /= pFactor; + position.xy = l.xy * l.zw + CameraPos.xy; + position.z *= zPerspectiveBias; + return position; +} + +void main() +{ + vec4 pos = LightMVP * gl_Vertex; + + tPos = applyPerspectiveDistortion(pos); + + gl_Position = vec4(tPos.xyz, 1.0); + gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; +} diff --git a/client/shaders/shadow_shaders/pass2_fragment.glsl b/client/shaders/shadow_shaders/pass2_fragment.glsl new file mode 100644 index 0000000..00b4f9f --- /dev/null +++ b/client/shaders/shadow_shaders/pass2_fragment.glsl @@ -0,0 +1,23 @@ +uniform sampler2D ShadowMapClientMap; +#ifdef COLORED_SHADOWS +uniform sampler2D ShadowMapClientMapTraslucent; +#endif +uniform sampler2D ShadowMapSamplerdynamic; + +void main() { + +#ifdef COLORED_SHADOWS + vec2 first_depth = texture2D(ShadowMapClientMap, gl_TexCoord[0].st).rg; + vec2 depth_splitdynamics = vec2(texture2D(ShadowMapSamplerdynamic, gl_TexCoord[2].st).r, 0.0); + if (first_depth.r > depth_splitdynamics.r) + first_depth = depth_splitdynamics; + vec2 depth_color = texture2D(ShadowMapClientMapTraslucent, gl_TexCoord[1].st).rg; + gl_FragColor = vec4(first_depth.r, first_depth.g, depth_color.r, depth_color.g); +#else + float first_depth = texture2D(ShadowMapClientMap, gl_TexCoord[0].st).r; + float depth_splitdynamics = texture2D(ShadowMapSamplerdynamic, gl_TexCoord[2].st).r; + first_depth = min(first_depth, depth_splitdynamics); + gl_FragColor = vec4(first_depth, 0.0, 0.0, 1.0); +#endif + +} diff --git a/client/shaders/shadow_shaders/pass2_vertex.glsl b/client/shaders/shadow_shaders/pass2_vertex.glsl new file mode 100644 index 0000000..ac445c9 --- /dev/null +++ b/client/shaders/shadow_shaders/pass2_vertex.glsl @@ -0,0 +1,9 @@ + +void main() +{ + vec4 uv = vec4(gl_Vertex.xyz, 1.0) * 0.5 + 0.5; + gl_TexCoord[0] = uv; + gl_TexCoord[1] = uv; + gl_TexCoord[2] = uv; + gl_Position = vec4(gl_Vertex.xyz, 1.0); +} diff --git a/client/shaders/stars_shader/opengl_fragment.glsl b/client/shaders/stars_shader/opengl_fragment.glsl new file mode 100644 index 0000000..a9ed741 --- /dev/null +++ b/client/shaders/stars_shader/opengl_fragment.glsl @@ -0,0 +1,6 @@ +uniform vec4 starColor; + +void main(void) +{ + gl_FragColor = starColor; +} diff --git a/client/shaders/stars_shader/opengl_vertex.glsl b/client/shaders/stars_shader/opengl_vertex.glsl new file mode 100644 index 0000000..77c401f --- /dev/null +++ b/client/shaders/stars_shader/opengl_vertex.glsl @@ -0,0 +1,4 @@ +void main(void) +{ + gl_Position = mWorldViewProj * inVertexPosition; +} diff --git a/clientmods/preview/example.lua b/clientmods/preview/example.lua new file mode 100644 index 0000000..2f42eef --- /dev/null +++ b/clientmods/preview/example.lua @@ -0,0 +1,2 @@ +print("Loaded example file!, loading more examples") +dofile(core.get_modpath(core.get_current_modname()) .. "/examples/first.lua") diff --git a/clientmods/preview/examples/first.lua b/clientmods/preview/examples/first.lua new file mode 100644 index 0000000..c24f461 --- /dev/null +++ b/clientmods/preview/examples/first.lua @@ -0,0 +1 @@ +print("loaded first.lua example file") diff --git a/clientmods/preview/init.lua b/clientmods/preview/init.lua new file mode 100644 index 0000000..46d59ec --- /dev/null +++ b/clientmods/preview/init.lua @@ -0,0 +1,206 @@ +local modname = assert(core.get_current_modname()) +local modstorage = core.get_mod_storage() +local mod_channel + +dofile(core.get_modpath(modname) .. "example.lua") + +core.register_on_shutdown(function() + print("[PREVIEW] shutdown client") +end) +local id = nil + +do + local server_info = core.get_server_info() + print("Server version: " .. server_info.protocol_version) + print("Server ip: " .. server_info.ip) + print("Server address: " .. server_info.address) + print("Server port: " .. server_info.port) + + print("CSM restrictions: " .. dump(core.get_csm_restrictions())) + + local l1, l2 = core.get_language() + print("Configured language: " .. l1 .. " / " .. l2) +end + +mod_channel = core.mod_channel_join("experimental_preview") + +core.after(4, function() + if mod_channel:is_writeable() then + mod_channel:send_all("preview talk to experimental") + end +end) + +core.after(1, function() + print("armor: " .. dump(core.localplayer:get_armor_groups())) + id = core.localplayer:hud_add({ + hud_elem_type = "text", + name = "example", + number = 0xff0000, + position = {x=0, y=1}, + offset = {x=8, y=-8}, + text = "You are using the preview mod", + scale = {x=200, y=60}, + alignment = {x=1, y=-1}, + }) +end) + +core.register_on_modchannel_message(function(channel, sender, message) + print("[PREVIEW][modchannels] Received message `" .. message .. "` on channel `" + .. channel .. "` from sender `" .. sender .. "`") + core.after(1, function() + mod_channel:send_all("CSM preview received " .. message) + end) +end) + +core.register_on_modchannel_signal(function(channel, signal) + print("[PREVIEW][modchannels] Received signal id `" .. signal .. "` on channel `" + .. channel) +end) + +core.register_on_inventory_open(function(inventory) + print("INVENTORY OPEN") + print(dump(inventory)) + return false +end) + +core.register_on_placenode(function(pointed_thing, node) + print("The local player place a node!") + print("pointed_thing :" .. dump(pointed_thing)) + print("node placed :" .. dump(node)) + return false +end) + +core.register_on_item_use(function(itemstack, pointed_thing) + print("The local player used an item!") + print("pointed_thing :" .. dump(pointed_thing)) + print("item = " .. itemstack:get_name()) + + if not itemstack:is_empty() then + return false + end + + local pos = core.camera:get_pos() + local pos2 = vector.add(pos, vector.multiply(core.camera:get_look_dir(), 100)) + + local rc = core.raycast(pos, pos2) + local i = rc:next() + print("[PREVIEW] raycast next: " .. dump(i)) + if i then + print("[PREVIEW] line of sight: " .. (core.line_of_sight(pos, i.above) and "yes" or "no")) + + local n1 = core.find_nodes_in_area(pos, i.under, {"default:stone"}) + local n2 = core.find_nodes_in_area_under_air(pos, i.under, {"default:stone"}) + print(("[PREVIEW] found %s nodes, %s nodes under air"):format( + n1 and #n1 or "?", n2 and #n2 or "?")) + end + + return false +end) + +-- This is an example function to ensure it's working properly, should be removed before merge +core.register_on_receiving_chat_message(function(message) + print("[PREVIEW] Received message " .. message) + return false +end) + +-- This is an example function to ensure it's working properly, should be removed before merge +core.register_on_sending_chat_message(function(message) + print("[PREVIEW] Sending message " .. message) + return false +end) + +core.register_on_chatcommand(function(command, params) + print("[PREVIEW] caught command '"..command.."'. Parameters: '"..params.."'") +end) + +-- This is an example function to ensure it's working properly, should be removed before merge +core.register_on_hp_modification(function(hp) + print("[PREVIEW] HP modified " .. hp) +end) + +-- This is an example function to ensure it's working properly, should be removed before merge +core.register_on_damage_taken(function(hp) + print("[PREVIEW] Damage taken " .. hp) +end) + +-- This is an example function to ensure it's working properly, should be removed before merge +core.register_chatcommand("dump", { + func = function(param) + return true, dump(_G) + end, +}) + +local function preview_minimap() + local minimap = core.ui.minimap + if not minimap then + print("[PREVIEW] Minimap is disabled. Skipping.") + return + end + minimap:set_mode(4) + minimap:show() + minimap:set_pos({x=5, y=50, z=5}) + minimap:set_shape(math.random(0, 1)) + + print("[PREVIEW] Minimap: mode => " .. dump(minimap:get_mode()) .. + " position => " .. dump(minimap:get_pos()) .. + " angle => " .. dump(minimap:get_angle())) +end + +core.after(2, function() + print("[PREVIEW] loaded " .. modname .. " mod") + modstorage:set_string("current_mod", modname) + assert(modstorage:get_string("current_mod") == modname) + preview_minimap() +end) + +core.after(5, function() + if core.ui.minimap then + core.ui.minimap:show() + end + + print("[PREVIEW] Time of day " .. core.get_timeofday()) + + print("[PREVIEW] Node level: " .. core.get_node_level({x=0, y=20, z=0}) .. + " max level " .. core.get_node_max_level({x=0, y=20, z=0})) + + print("[PREVIEW] Find node near: " .. dump(core.find_node_near({x=0, y=20, z=0}, 10, + {"group:tree", "default:dirt", "default:stone"}))) + + print("[PREVIEW] Settings: preview_csm_test_setting = " .. + tostring(core.settings:get_bool("preview_csm_test_setting", false))) +end) + +core.register_on_dignode(function(pos, node) + print("The local player dug a node!") + print("pos:" .. dump(pos)) + print("node:" .. dump(node)) + return false +end) + +core.register_on_punchnode(function(pos, node) + print("The local player punched a node!") + local itemstack = core.localplayer:get_wielded_item() + print(dump(itemstack:to_table())) + print("pos:" .. dump(pos)) + print("node:" .. dump(node)) + local meta = core.get_meta(pos) + print("punched meta: " .. (meta and dump(meta:to_table()) or "(missing)")) + return false +end) + +core.register_chatcommand("privs", { + func = function(param) + return true, core.privs_to_string(minetest.get_privilege_list()) + end, +}) + +core.register_chatcommand("text", { + func = function(param) + return core.localplayer:hud_change(id, "text", param) + end, +}) + + +core.register_on_mods_loaded(function() + core.log("Yeah preview mod is loaded with other CSM mods.") +end) diff --git a/clientmods/preview/mod.conf b/clientmods/preview/mod.conf new file mode 100644 index 0000000..23a5c3e --- /dev/null +++ b/clientmods/preview/mod.conf @@ -0,0 +1 @@ +name = preview diff --git a/clientmods/preview/settingtypes.txt b/clientmods/preview/settingtypes.txt new file mode 100644 index 0000000..fea9e71 --- /dev/null +++ b/clientmods/preview/settingtypes.txt @@ -0,0 +1 @@ +preview_csm_test_setting (Test CSM setting) bool false \ No newline at end of file diff --git a/cmake/Modules/FindCURL.cmake b/cmake/Modules/FindCURL.cmake new file mode 100644 index 0000000..43aaf3e --- /dev/null +++ b/cmake/Modules/FindCURL.cmake @@ -0,0 +1,16 @@ +mark_as_advanced(CURL_LIBRARY CURL_INCLUDE_DIR) + +find_library(CURL_LIBRARY NAMES curl libcurl) +find_path(CURL_INCLUDE_DIR NAMES curl/curl.h) + +if(WIN32) + # If VCPKG_APPLOCAL_DEPS is ON, dll's are automatically handled by VCPKG + if(NOT VCPKG_APPLOCAL_DEPS) + find_file(CURL_DLL NAMES libcurl-4.dll libcurl.dll + DOC "Path to the cURL DLL (for installation)") + mark_as_advanced(CURL_DLL) + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(CURL DEFAULT_MSG CURL_LIBRARY CURL_INCLUDE_DIR) diff --git a/cmake/Modules/FindGMP.cmake b/cmake/Modules/FindGMP.cmake new file mode 100644 index 0000000..190b7c5 --- /dev/null +++ b/cmake/Modules/FindGMP.cmake @@ -0,0 +1,25 @@ +option(ENABLE_SYSTEM_GMP "Use GMP from system" TRUE) +mark_as_advanced(GMP_LIBRARY GMP_INCLUDE_DIR) +set(USE_SYSTEM_GMP FALSE) + +if(ENABLE_SYSTEM_GMP) + find_library(GMP_LIBRARY NAMES gmp) + find_path(GMP_INCLUDE_DIR NAMES gmp.h) + + if(GMP_LIBRARY AND GMP_INCLUDE_DIR) + message (STATUS "Using GMP provided by system.") + set(USE_SYSTEM_GMP TRUE) + else() + message (STATUS "Detecting GMP from system failed.") + endif() +endif() + +if(NOT USE_SYSTEM_GMP) + message(STATUS "Using bundled mini-gmp library.") + set(GMP_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lib/gmp) + set(GMP_LIBRARY gmp) + add_subdirectory(lib/gmp) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GMP DEFAULT_MSG GMP_LIBRARY GMP_INCLUDE_DIR) diff --git a/cmake/Modules/FindGettextLib.cmake b/cmake/Modules/FindGettextLib.cmake new file mode 100644 index 0000000..b768182 --- /dev/null +++ b/cmake/Modules/FindGettextLib.cmake @@ -0,0 +1,69 @@ + +set(CUSTOM_GETTEXT_PATH "${PROJECT_SOURCE_DIR}/../../gettext" + CACHE FILEPATH "path to custom gettext") + +find_path(GETTEXT_INCLUDE_DIR + NAMES libintl.h + PATHS "${CUSTOM_GETTEXT_PATH}/include" + DOC "GetText include directory") + +find_program(GETTEXT_MSGFMT + NAMES msgfmt + PATHS "${CUSTOM_GETTEXT_PATH}/bin" + DOC "Path to msgfmt") + +set(GETTEXT_REQUIRED_VARS GETTEXT_INCLUDE_DIR GETTEXT_MSGFMT) + +if(APPLE) + find_library(GETTEXT_LIBRARY + NAMES libintl.a + PATHS "${CUSTOM_GETTEXT_PATH}/lib" + DOC "GetText library") + + find_library(ICONV_LIBRARY + NAMES libiconv.dylib + PATHS "/usr/lib" + DOC "IConv library") + set(GETTEXT_REQUIRED_VARS ${GETTEXT_REQUIRED_VARS} GETTEXT_LIBRARY ICONV_LIBRARY) +endif(APPLE) + +# Modern Linux, as well as OSX, does not require special linking because +# GetText is part of glibc. +# TODO: check the requirements on other BSDs and older Linux +if(WIN32) + if(MSVC) + set(GETTEXT_LIB_NAMES + libintl.lib intl.lib libintl3.lib intl3.lib) + else() + set(GETTEXT_LIB_NAMES + libintl.dll.a intl.dll.a libintl3.dll.a intl3.dll.a) + endif() + find_library(GETTEXT_LIBRARY + NAMES ${GETTEXT_LIB_NAMES} + PATHS "${CUSTOM_GETTEXT_PATH}/lib" + DOC "GetText library") +endif(WIN32) + + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(GettextLib DEFAULT_MSG ${GETTEXT_REQUIRED_VARS}) + + +if(GETTEXTLIB_FOUND) + # BSD variants require special linkage as they don't use glibc + if(${CMAKE_SYSTEM_NAME} MATCHES "BSD|DragonFly") + set(GETTEXT_LIBRARY "intl") + endif() + + set(GETTEXT_PO_PATH ${CMAKE_SOURCE_DIR}/po) + set(GETTEXT_MO_BUILD_PATH ${CMAKE_BINARY_DIR}/locale//LC_MESSAGES) + set(GETTEXT_MO_DEST_PATH ${LOCALEDIR}//LC_MESSAGES) + file(GLOB GETTEXT_AVAILABLE_LOCALES RELATIVE ${GETTEXT_PO_PATH} "${GETTEXT_PO_PATH}/*") + list(REMOVE_ITEM GETTEXT_AVAILABLE_LOCALES minetest.pot) + list(REMOVE_ITEM GETTEXT_AVAILABLE_LOCALES timestamp) + macro(SET_MO_PATHS _buildvar _destvar _locale) + string(REPLACE "" ${_locale} ${_buildvar} ${GETTEXT_MO_BUILD_PATH}) + string(REPLACE "" ${_locale} ${_destvar} ${GETTEXT_MO_DEST_PATH}) + endmacro() +endif() + diff --git a/cmake/Modules/FindJson.cmake b/cmake/Modules/FindJson.cmake new file mode 100644 index 0000000..cce2d38 --- /dev/null +++ b/cmake/Modules/FindJson.cmake @@ -0,0 +1,25 @@ +# Look for JsonCpp, with fallback to bundeled version + +mark_as_advanced(JSON_LIBRARY JSON_INCLUDE_DIR) +option(ENABLE_SYSTEM_JSONCPP "Enable using a system-wide JsonCpp" TRUE) +set(USE_SYSTEM_JSONCPP FALSE) + +if(ENABLE_SYSTEM_JSONCPP) + find_library(JSON_LIBRARY NAMES jsoncpp) + find_path(JSON_INCLUDE_DIR json/allocator.h PATH_SUFFIXES jsoncpp) + + if(JSON_LIBRARY AND JSON_INCLUDE_DIR) + message(STATUS "Using JsonCpp provided by system.") + set(USE_SYSTEM_JSONCPP TRUE) + endif() +endif() + +if(NOT USE_SYSTEM_JSONCPP) + message(STATUS "Using bundled JsonCpp library.") + set(JSON_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lib/jsoncpp) + set(JSON_LIBRARY jsoncpp) + add_subdirectory(lib/jsoncpp) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Json DEFAULT_MSG JSON_LIBRARY JSON_INCLUDE_DIR) diff --git a/cmake/Modules/FindLua.cmake b/cmake/Modules/FindLua.cmake new file mode 100644 index 0000000..be5d92d --- /dev/null +++ b/cmake/Modules/FindLua.cmake @@ -0,0 +1,28 @@ +# Look for Lua library to use +# This selects LuaJIT by default + +option(ENABLE_LUAJIT "Enable LuaJIT support" TRUE) +set(USE_LUAJIT FALSE) +option(REQUIRE_LUAJIT "Require LuaJIT support" FALSE) +if(REQUIRE_LUAJIT) + set(ENABLE_LUAJIT TRUE) +endif() +if(ENABLE_LUAJIT) + find_package(LuaJIT) + if(LUAJIT_FOUND) + set(USE_LUAJIT TRUE) + message (STATUS "Using LuaJIT provided by system.") + elseif(REQUIRE_LUAJIT) + message(FATAL_ERROR "LuaJIT not found whereas REQUIRE_LUAJIT=\"TRUE\" is used.\n" + "To continue, either install LuaJIT or do not use REQUIRE_LUAJIT=\"TRUE\".") + endif() +else() + message (STATUS "LuaJIT detection disabled! (ENABLE_LUAJIT=0)") +endif() + +if(NOT USE_LUAJIT) + message(STATUS "LuaJIT not found, using bundled Lua.") + set(LUA_LIBRARY lua) + set(LUA_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lib/lua/src) + add_subdirectory(lib/lua) +endif() diff --git a/cmake/Modules/FindLuaJIT.cmake b/cmake/Modules/FindLuaJIT.cmake new file mode 100644 index 0000000..217415d --- /dev/null +++ b/cmake/Modules/FindLuaJIT.cmake @@ -0,0 +1,53 @@ +# Locate LuaJIT library +# This module defines +# LUAJIT_FOUND, if false, do not try to link to Lua +# LUA_LIBRARY, where to find the lua library +# LUA_INCLUDE_DIR, where to find lua.h +# +# This module is similar to FindLua51.cmake except that it finds LuaJit instead. + +FIND_PATH(LUA_INCLUDE_DIR luajit.h + HINTS + $ENV{LUA_DIR} + PATH_SUFFIXES include/luajit-2.1 include/luajit-2.0 include/luajit-5_1-2.1 include/luajit-5_1-2.0 include luajit + PATHS + ~/Library/Frameworks + /Library/Frameworks + /sw # Fink + /opt/local # DarwinPorts + /opt/csw # Blastwave + /opt +) + +# Test if running on vcpkg toolchain +if(DEFINED VCPKG_TARGET_TRIPLET AND DEFINED VCPKG_APPLOCAL_DEPS) + # On vcpkg luajit is 'lua51' and normal lua is 'lua' + FIND_LIBRARY(LUA_LIBRARY + NAMES lua51 + HINTS + $ENV{LUA_DIR} + PATH_SUFFIXES lib + ) +else() + FIND_LIBRARY(LUA_LIBRARY + NAMES luajit-5.1 + HINTS + $ENV{LUA_DIR} + PATH_SUFFIXES lib64 lib + PATHS + ~/Library/Frameworks + /Library/Frameworks + /sw + /opt/local + /opt/csw + /opt + ) +endif() + +INCLUDE(FindPackageHandleStandardArgs) +# handle the QUIETLY and REQUIRED arguments and set LUAJIT_FOUND to TRUE if +# all listed variables exist +FIND_PACKAGE_HANDLE_STANDARD_ARGS(LuaJIT + REQUIRED_VARS LUA_LIBRARY LUA_INCLUDE_DIR) + +MARK_AS_ADVANCED(LUA_INCLUDE_DIR LUA_LIBRARY) diff --git a/cmake/Modules/FindNcursesw.cmake b/cmake/Modules/FindNcursesw.cmake new file mode 100644 index 0000000..e572c70 --- /dev/null +++ b/cmake/Modules/FindNcursesw.cmake @@ -0,0 +1,204 @@ +#.rst: +# FindNcursesw +# ------------ +# +# Find the ncursesw (wide ncurses) include file and library. +# +# Based on FindCurses.cmake which comes with CMake. +# +# Checks for ncursesw first. If not found, it then executes the +# regular old FindCurses.cmake to look for for ncurses (or curses). +# +# +# Result Variables +# ^^^^^^^^^^^^^^^^ +# +# This module defines the following variables: +# +# ``CURSES_FOUND`` +# True if curses is found. +# ``NCURSESW_FOUND`` +# True if ncursesw is found. +# ``CURSES_INCLUDE_DIRS`` +# The include directories needed to use Curses. +# ``CURSES_LIBRARIES`` +# The libraries needed to use Curses. +# ``CURSES_HAVE_CURSES_H`` +# True if curses.h is available. +# ``CURSES_HAVE_NCURSES_H`` +# True if ncurses.h is available. +# ``CURSES_HAVE_NCURSES_NCURSES_H`` +# True if ``ncurses/ncurses.h`` is available. +# ``CURSES_HAVE_NCURSES_CURSES_H`` +# True if ``ncurses/curses.h`` is available. +# ``CURSES_HAVE_NCURSESW_NCURSES_H`` +# True if ``ncursesw/ncurses.h`` is available. +# ``CURSES_HAVE_NCURSESW_CURSES_H`` +# True if ``ncursesw/curses.h`` is available. +# +# Set ``CURSES_NEED_NCURSES`` to ``TRUE`` before the +# ``find_package(Ncursesw)`` call if NCurses functionality is required. +# +#============================================================================= +# Copyright 2001-2014 Kitware, Inc. +# modifications: Copyright 2015 kahrl +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the names of Kitware, Inc., the Insight Software Consortium, +# nor the names of their contributors may be used to endorse or promote +# products derived from this software without specific prior written +# permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# ------------------------------------------------------------------------------ +# +# The above copyright and license notice applies to distributions of +# CMake in source and binary form. Some source files contain additional +# notices of original copyright by their contributors; see each source +# for details. Third-party software packages supplied with CMake under +# compatible licenses provide their own copyright notices documented in +# corresponding subdirectories. +# +# ------------------------------------------------------------------------------ +# +# CMake was initially developed by Kitware with the following sponsorship: +# +# * National Library of Medicine at the National Institutes of Health +# as part of the Insight Segmentation and Registration Toolkit (ITK). +# +# * US National Labs (Los Alamos, Livermore, Sandia) ASC Parallel +# Visualization Initiative. +# +# * National Alliance for Medical Image Computing (NAMIC) is funded by the +# National Institutes of Health through the NIH Roadmap for Medical Research, +# Grant U54 EB005149. +# +# * Kitware, Inc. +#============================================================================= + +include(CheckLibraryExists) + +find_library(CURSES_NCURSESW_LIBRARY NAMES ncursesw + DOC "Path to libncursesw.so or .lib or .a") + +set(CURSES_USE_NCURSES FALSE) +set(CURSES_USE_NCURSESW FALSE) + +if(CURSES_NCURSESW_LIBRARY) + set(CURSES_USE_NCURSES TRUE) + set(CURSES_USE_NCURSESW TRUE) +endif() + +if(CURSES_USE_NCURSESW) + get_filename_component(_cursesLibDir "${CURSES_NCURSESW_LIBRARY}" PATH) + get_filename_component(_cursesParentDir "${_cursesLibDir}" PATH) + + find_path(CURSES_INCLUDE_PATH + NAMES ncursesw/ncurses.h ncursesw/curses.h ncurses.h curses.h + HINTS "${_cursesParentDir}/include" + ) + + # Previous versions of FindCurses provided these values. + if(NOT DEFINED CURSES_LIBRARY) + set(CURSES_LIBRARY "${CURSES_NCURSESW_LIBRARY}") + endif() + + CHECK_LIBRARY_EXISTS("${CURSES_NCURSESW_LIBRARY}" + cbreak "" CURSES_NCURSESW_HAS_CBREAK) + if(NOT CURSES_NCURSESW_HAS_CBREAK) + find_library(CURSES_EXTRA_LIBRARY tinfo HINTS "${_cursesLibDir}" + DOC "Path to libtinfo.so or .lib or .a") + find_library(CURSES_EXTRA_LIBRARY tinfo ) + endif() + + # Report whether each possible header name exists in the include directory. + if(NOT DEFINED CURSES_HAVE_NCURSESW_NCURSES_H) + if(EXISTS "${CURSES_INCLUDE_PATH}/ncursesw/ncurses.h") + set(CURSES_HAVE_NCURSESW_NCURSES_H "${CURSES_INCLUDE_PATH}/ncursesw/ncurses.h") + else() + set(CURSES_HAVE_NCURSESW_NCURSES_H "CURSES_HAVE_NCURSESW_NCURSES_H-NOTFOUND") + endif() + endif() + if(NOT DEFINED CURSES_HAVE_NCURSESW_CURSES_H) + if(EXISTS "${CURSES_INCLUDE_PATH}/ncursesw/curses.h") + set(CURSES_HAVE_NCURSESW_CURSES_H "${CURSES_INCLUDE_PATH}/ncursesw/curses.h") + else() + set(CURSES_HAVE_NCURSESW_CURSES_H "CURSES_HAVE_NCURSESW_CURSES_H-NOTFOUND") + endif() + endif() + if(NOT DEFINED CURSES_HAVE_NCURSES_H) + if(EXISTS "${CURSES_INCLUDE_PATH}/ncurses.h") + set(CURSES_HAVE_NCURSES_H "${CURSES_INCLUDE_PATH}/ncurses.h") + else() + set(CURSES_HAVE_NCURSES_H "CURSES_HAVE_NCURSES_H-NOTFOUND") + endif() + endif() + if(NOT DEFINED CURSES_HAVE_CURSES_H) + if(EXISTS "${CURSES_INCLUDE_PATH}/curses.h") + set(CURSES_HAVE_CURSES_H "${CURSES_INCLUDE_PATH}/curses.h") + else() + set(CURSES_HAVE_CURSES_H "CURSES_HAVE_CURSES_H-NOTFOUND") + endif() + endif() + + + find_library(CURSES_FORM_LIBRARY form HINTS "${_cursesLibDir}" + DOC "Path to libform.so or .lib or .a") + find_library(CURSES_FORM_LIBRARY form ) + + # Need to provide the *_LIBRARIES + set(CURSES_LIBRARIES ${CURSES_LIBRARY}) + + if(CURSES_EXTRA_LIBRARY) + set(CURSES_LIBRARIES ${CURSES_LIBRARIES} ${CURSES_EXTRA_LIBRARY}) + endif() + + if(CURSES_FORM_LIBRARY) + set(CURSES_LIBRARIES ${CURSES_LIBRARIES} ${CURSES_FORM_LIBRARY}) + endif() + + # Provide the *_INCLUDE_DIRS result. + set(CURSES_INCLUDE_DIRS ${CURSES_INCLUDE_PATH}) + set(CURSES_INCLUDE_DIR ${CURSES_INCLUDE_PATH}) # compatibility + + # handle the QUIETLY and REQUIRED arguments and set CURSES_FOUND to TRUE if + # all listed variables are TRUE + include(FindPackageHandleStandardArgs) + FIND_PACKAGE_HANDLE_STANDARD_ARGS(Ncursesw DEFAULT_MSG + CURSES_LIBRARY CURSES_INCLUDE_PATH) + set(CURSES_FOUND ${NCURSESW_FOUND}) + +else() + find_package(Curses) + set(NCURSESW_FOUND FALSE) +endif() + +mark_as_advanced( + CURSES_INCLUDE_PATH + CURSES_CURSES_LIBRARY + CURSES_NCURSES_LIBRARY + CURSES_NCURSESW_LIBRARY + CURSES_EXTRA_LIBRARY + CURSES_FORM_LIBRARY + ) diff --git a/cmake/Modules/FindSQLite3.cmake b/cmake/Modules/FindSQLite3.cmake new file mode 100644 index 0000000..8a66cb2 --- /dev/null +++ b/cmake/Modules/FindSQLite3.cmake @@ -0,0 +1,9 @@ +mark_as_advanced(SQLITE3_LIBRARY SQLITE3_INCLUDE_DIR) + +find_path(SQLITE3_INCLUDE_DIR sqlite3.h) + +find_library(SQLITE3_LIBRARY NAMES sqlite3) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SQLite3 DEFAULT_MSG SQLITE3_LIBRARY SQLITE3_INCLUDE_DIR) + diff --git a/cmake/Modules/FindVorbis.cmake b/cmake/Modules/FindVorbis.cmake new file mode 100644 index 0000000..222ddd9 --- /dev/null +++ b/cmake/Modules/FindVorbis.cmake @@ -0,0 +1,45 @@ +# - Find vorbis +# Find the native vorbis includes and libraries +# +# VORBIS_INCLUDE_DIR - where to find vorbis.h, etc. +# OGG_INCLUDE_DIR - where to find ogg/ogg.h, etc. +# VORBIS_LIBRARIES - List of libraries when using vorbis(file). +# VORBIS_FOUND - True if vorbis found. + +if(NOT GP2XWIZ) + if(VORBIS_INCLUDE_DIR) + # Already in cache, be silent + set(VORBIS_FIND_QUIETLY TRUE) + endif(VORBIS_INCLUDE_DIR) + find_path(OGG_INCLUDE_DIR ogg/ogg.h) + find_path(VORBIS_INCLUDE_DIR vorbis/vorbisfile.h) + # MSVC built ogg/vorbis may be named ogg_static and vorbis_static + find_library(OGG_LIBRARY NAMES ogg ogg_static) + find_library(VORBIS_LIBRARY NAMES vorbis vorbis_static) + find_library(VORBISFILE_LIBRARY NAMES vorbisfile vorbisfile_static) + # Handle the QUIETLY and REQUIRED arguments and set VORBIS_FOUND + # to TRUE if all listed variables are TRUE. + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Vorbis DEFAULT_MSG + OGG_INCLUDE_DIR VORBIS_INCLUDE_DIR + OGG_LIBRARY VORBIS_LIBRARY VORBISFILE_LIBRARY) +else(NOT GP2XWIZ) + find_path(VORBIS_INCLUDE_DIR tremor/ivorbisfile.h) + find_library(VORBIS_LIBRARY NAMES vorbis_dec) + find_package_handle_standard_args(Vorbis DEFAULT_MSG + VORBIS_INCLUDE_DIR VORBIS_LIBRARY) +endif(NOT GP2XWIZ) + +if(VORBIS_FOUND) + if(NOT GP2XWIZ) + set(VORBIS_LIBRARIES ${VORBISFILE_LIBRARY} ${VORBIS_LIBRARY} + ${OGG_LIBRARY}) + else(NOT GP2XWIZ) + set(VORBIS_LIBRARIES ${VORBIS_LIBRARY}) + endif(NOT GP2XWIZ) +else(VORBIS_FOUND) + set(VORBIS_LIBRARIES) +endif(VORBIS_FOUND) + +mark_as_advanced(OGG_INCLUDE_DIR VORBIS_INCLUDE_DIR) +mark_as_advanced(OGG_LIBRARY VORBIS_LIBRARY VORBISFILE_LIBRARY) diff --git a/cmake/Modules/FindZstd.cmake b/cmake/Modules/FindZstd.cmake new file mode 100644 index 0000000..e28e133 --- /dev/null +++ b/cmake/Modules/FindZstd.cmake @@ -0,0 +1,25 @@ +mark_as_advanced(ZSTD_LIBRARY ZSTD_INCLUDE_DIR) + +find_path(ZSTD_INCLUDE_DIR NAMES zstd.h) + +find_library(ZSTD_LIBRARY NAMES zstd) + +if(ZSTD_INCLUDE_DIR AND ZSTD_LIBRARY) + # Check that the API we use exists + include(CheckSymbolExists) + unset(HAVE_ZSTD_INITCSTREAM CACHE) + set(CMAKE_REQUIRED_INCLUDES ${ZSTD_INCLUDE_DIR}) + set(CMAKE_REQUIRED_LIBRARIES ${ZSTD_LIBRARY}) + check_symbol_exists(ZSTD_initCStream zstd.h HAVE_ZSTD_INITCSTREAM) + unset(CMAKE_REQUIRED_INCLUDES) + unset(CMAKE_REQUIRED_LIBRARIES) + + if(NOT HAVE_ZSTD_INITCSTREAM) + unset(ZSTD_INCLUDE_DIR CACHE) + unset(ZSTD_LIBRARY CACHE) + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Zstd DEFAULT_MSG ZSTD_LIBRARY ZSTD_INCLUDE_DIR) + diff --git a/cmake/Modules/GenerateVersion.cmake b/cmake/Modules/GenerateVersion.cmake new file mode 100644 index 0000000..ad0e382 --- /dev/null +++ b/cmake/Modules/GenerateVersion.cmake @@ -0,0 +1,26 @@ +# Always run during 'make' + +if(DEVELOPMENT_BUILD) + execute_process(COMMAND git rev-parse --short HEAD + WORKING_DIRECTORY "${GENERATE_VERSION_SOURCE_DIR}" + OUTPUT_VARIABLE VERSION_GITHASH OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(VERSION_GITHASH) + set(VERSION_GITHASH "${VERSION_STRING}-${VERSION_GITHASH}") + execute_process(COMMAND git diff-index --quiet HEAD + WORKING_DIRECTORY "${GENERATE_VERSION_SOURCE_DIR}" + RESULT_VARIABLE IS_DIRTY) + if(IS_DIRTY) + set(VERSION_GITHASH "${VERSION_GITHASH}-dirty") + endif() + message(STATUS "*** Detected Git version ${VERSION_GITHASH} ***") + endif() +endif() +if(NOT VERSION_GITHASH) + set(VERSION_GITHASH "${VERSION_STRING}") +endif() + +configure_file( + ${GENERATE_VERSION_SOURCE_DIR}/cmake_config_githash.h.in + ${GENERATE_VERSION_BINARY_DIR}/cmake_config_githash.h) + diff --git a/cmake/Modules/MinetestFindIrrlichtHeaders.cmake b/cmake/Modules/MinetestFindIrrlichtHeaders.cmake new file mode 100644 index 0000000..e434b58 --- /dev/null +++ b/cmake/Modules/MinetestFindIrrlichtHeaders.cmake @@ -0,0 +1,18 @@ +# Locate IrrlichtMt headers on system. + +find_path(IRRLICHT_INCLUDE_DIR NAMES irrlicht.h + DOC "Path to the directory with IrrlichtMt includes" + PATHS + /usr/local/include/irrlichtmt + /usr/include/irrlichtmt + /system/develop/headers/irrlichtmt #Haiku + PATH_SUFFIXES "include/irrlichtmt" +) + +# Handholding for users +if(IRRLICHT_INCLUDE_DIR AND (NOT IS_DIRECTORY "${IRRLICHT_INCLUDE_DIR}" OR + NOT EXISTS "${IRRLICHT_INCLUDE_DIR}/irrlicht.h")) + message(WARNING "IRRLICHT_INCLUDE_DIR was set to ${IRRLICHT_INCLUDE_DIR} " + "but irrlicht.h does not exist inside. The path will not be used.") + unset(IRRLICHT_INCLUDE_DIR CACHE) +endif() diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in new file mode 100644 index 0000000..ae36fd6 --- /dev/null +++ b/doc/Doxyfile.in @@ -0,0 +1,44 @@ +# Project properties +PROJECT_NAME = @PROJECT_NAME_CAPITALIZED@ +PROJECT_NUMBER = @VERSION_STRING@ +PROJECT_LOGO = @CMAKE_CURRENT_SOURCE_DIR@/misc/minetest.svg + +# Parsing +JAVADOC_AUTOBRIEF = YES +EXTRACT_ALL = YES +EXTRACT_PRIVATE = YES +EXTRACT_STATIC = YES +SORT_MEMBERS_CTORS_1ST = YES +WARN_IF_UNDOCUMENTED = NO +BUILTIN_STL_SUPPORT = YES +PREDEFINED = "USE_SPATIAL=1" \ + "USE_LEVELDB=1" \ + "USE_REDIS=1" \ + "USE_SOUND=1" \ + "USE_CURL=1" \ + "USE_GETTEXT=1" + +# Input +RECURSIVE = YES +STRIP_FROM_PATH = @CMAKE_CURRENT_SOURCE_DIR@/src +INPUT = @CMAKE_CURRENT_SOURCE_DIR@/doc/main_page.dox \ + @CMAKE_CURRENT_SOURCE_DIR@/src/ + +# Dot graphs +HAVE_DOT = @DOXYGEN_DOT_FOUND@ +CALL_GRAPH = YES +CALLER_GRAPH = YES +MAX_DOT_GRAPH_DEPTH = 3 +DOT_MULTI_TARGETS = YES +DOT_IMAGE_FORMAT = svg + +# Output +GENERATE_LATEX = NO +REFERENCED_BY_RELATION = YES +REFERENCES_RELATION = YES +SEARCHENGINE = YES +DISABLE_INDEX = YES +GENERATE_TREEVIEW = YES +HTML_DYNAMIC_SECTIONS = YES +HTML_TIMESTAMP = YES + diff --git a/doc/README.android b/doc/README.android new file mode 100644 index 0000000..3833688 --- /dev/null +++ b/doc/README.android @@ -0,0 +1,81 @@ +Minetest: Android version +========================= + +Controls +-------- +The Android port doesn't support everything you can do on PC due to the +limited capabilities of common devices. What can be done is described +below: + +While you're playing the game normally (that is, no menu or inventory is +shown), the following controls are available: +* Look around: touch screen and slide finger +* double tap: place a node or use selected item +* long tap: dig node +* touch shown buttons: press button +* Buttons: +** left upper corner: chat +** right lower corner: jump +** right lower corner: crouch +** left lower corner: walk/step... + left up right + down +** left lower corner: display inventory + +When a menu or inventory is displayed: +* double tap outside menu area: close menu +* tap on an item stack: select that stack +* tap on an empty slot: if you selected a stack already, that stack is placed here +* drag and drop: touch stack and hold finger down, move the stack to another + slot, tap another finger while keeping first finger on screen + --> places a single item from dragged stack into current (first touched) slot + +Special settings +---------------- +There are some settings especially useful for Android users. Minetest's config +file can usually be found at /mnt/sdcard/Minetest. + +* gui_scaling: this is a user-specified scaling factor for the GUI- In case + main menu is too big or small on your device, try changing this + value. + +Requirements +------------ + +In order to build, your PC has to be set up to build Minetest in the usual +manner (see the regular Minetest documentation for how to get this done). +In addition to what is required for Minetest in general, you will need the +following software packages. The version number in parenthesis denotes the +version that was tested at the time this README was drafted; newer/older +versions may or may not work. + +* Android SDK 29 +* Android NDK r21 +* Android Studio 3 [optional] + +Additionally, you'll need to have an Internet connection available on the +build system, as the Android build will download some source packages. + +Build +----- + +The new build system Minetest Android is fully functional and is designed to +speed up and simplify the work, as well as adding the possibility of +cross-platform build. +You can use `./gradlew assemblerelease` or `./gradlew assembledebug` from the +command line or use Android Studio and click the build button. + +When using gradlew, the newest NDK will be downloaded and installed +automatically. Or you can create a `local.properties` file and specify +`sdk.dir` and `ndk.dir` yourself. + +* In order to make a release build you'll have to have a keystore setup to sign + the resulting apk package. How this is done is not part of this README. There + are different tutorials on the web explaining how to do it + - choose one yourself. + +* Once your keystore is setup, enter the android subdirectory and create a new + file "ant.properties" there. Add following lines to that file: + + > key.store= + > key.alias=Minetest diff --git a/doc/breakages.md b/doc/breakages.md new file mode 100644 index 0000000..f7078f1 --- /dev/null +++ b/doc/breakages.md @@ -0,0 +1,8 @@ +# Minetest Major Breakages List + +This document contains a list of breaking changes to be made in the next major version. + +* Remove attachment space multiplier (*10) +* `get_sky()` returns a table (without arg) +* `game.conf` name/id mess +* remove `depends.txt` / `description.txt` (would simplify ContentDB and Minetest code a little) diff --git a/doc/builtin_entities.txt b/doc/builtin_entities.txt new file mode 100644 index 0000000..be3f733 --- /dev/null +++ b/doc/builtin_entities.txt @@ -0,0 +1,101 @@ +# Builtin Entities +Minetest registers two entities by default: Falling nodes and dropped items. +This document describes how they behave and what you can do with them. + +## Falling node (`__builtin:falling_node`) + +This entity is created by `minetest.check_for_falling` in place of a node +with the special group `falling_node=1`. Falling nodes can also be created +artificially with `minetest.spawn_falling_node`. + +Needs manual initialization when spawned using `/spawnentity`. + +Default behaviour: + +* Falls down in a straight line (gravity = `movement_gravity` setting) +* Collides with `walkable` node +* Collides with all physical objects except players +* If the node group `float=1` is set, it also collides with liquid nodes +* When it hits a solid (=`walkable`) node, it will try to place itself as a + node, replacing the node above. + * If the falling node cannot replace the destination node, it is dropped. + * If the destination node is a leveled node (`paramtype2="leveled"`) of the + same node name, the levels of both are summed. + +### Entity fields + +* `set_node(self, node[, meta])` + * Function to initialize the falling node + * `node` and `meta` are explained below. + * The `meta` argument is optional. +* `node`: Node table of the node (`name`, `param1`, `param2`) that this + entity represents. Read-only. +* `meta`: Node metadata of the falling node. Will be used when the falling + nodes tries to place itself as a node. Read-only. + +### Rendering / supported nodes + +Falling nodes have visuals to look as close as possible to the original node. +This works for most drawtypes, but there are limitations. + +Supported drawtypes: + +* `normal` +* `signlike` +* `torchlike` +* `nodebox` +* `raillike` +* `glasslike` +* `glasslike_framed` +* `glasslike_framed_optional` +* `allfaces` +* `allfaces_optional` +* `firelike` +* `mesh` +* `fencelike` +* `liquid` +* `airlike` (not pointable) + +Other drawtypes still kinda work, but they might look weird. + +Supported `paramtype2` values: + +* `wallmounted` +* `facedir` +* `colorwallmounted` +* `colorfacedir` +* `color` + +## Dropped item stack (`__builtin:item`) + +This is an item stack in a collectable form. + +Common cases that spawn a dropped item: + +* Item dropped by player +* The root node of a node with the group `attached_node=1` is removed +* `minetest.add_item` is called + +Needs manual initialization when spawned using `/spawnentity`. + +### Behavior + +* Players can collect it by punching +* Lifespan is defined by the setting `item_entity_ttl` +* Slides on `slippery` nodes +* Subject to gravity (uses `movement_gravity` setting) +* Collides with `walkable` nodes +* Does not collide physical objects +* When it's inside a solid (`walkable=true`) node, it tries to escape to a + neighboring non-solid (`walkable=false`) node + +### Entity fields + +* `set_item(self, item)`: + * Function to initialize the dropped item + * `item` (type `ItemStack`) specifies the item to represent +* `age`: Age in seconds. Behaviour according to the setting `item_entity_ttl` +* `itemstring`: Itemstring of the item that this item entity represents. + Read-only. + +Other fields are for internal use only. diff --git a/doc/client_lua_api.txt b/doc/client_lua_api.txt new file mode 100644 index 0000000..8a450ba --- /dev/null +++ b/doc/client_lua_api.txt @@ -0,0 +1,1527 @@ +Minetest Lua Client Modding API Reference 5.6.0 +================================================ +* More information at +* Developer Wiki: + +Introduction +------------ + +** WARNING: The client API is currently unstable, and may break/change without warning. ** + +Content and functionality can be added to Minetest 0.4.15-dev+ by using Lua +scripting in run-time loaded mods. + +A mod is a self-contained bunch of scripts, textures and other related +things that is loaded by and interfaces with Minetest. + +Transferring client-sided mods from the server to the client is planned, but not implemented yet. + +If you see a deficiency in the API, feel free to attempt to add the +functionality in the engine and API. You can send such improvements as +source code patches on GitHub (https://github.com/minetest/minetest). + +Programming in Lua +------------------ +If you have any difficulty in understanding this, please read +[Programming in Lua](http://www.lua.org/pil/). + +Startup +------- +Mods are loaded during client startup from the mod load paths by running +the `init.lua` scripts in a shared environment. + +In order to load client-side mods, the following conditions need to be satisfied: + +1) `$path_user/minetest.conf` contains the setting `enable_client_modding = true` + +2) The client-side mod located in `$path_user/clientmods/` is added to + `$path_user/clientmods/mods.conf` as `load_mod_ = true`. + +Note: Depending on the remote server's settings, client-side mods might not +be loaded or have limited functionality. See setting `csm_restriction_flags` for reference. + +Paths +----- +* `RUN_IN_PLACE=1` (Windows release, local build) + * `$path_user`: `` + * `$path_share`: `` +* `RUN_IN_PLACE=0`: (Linux release) + * `$path_share`: + * Linux: `/usr/share/minetest` + * Windows: `/minetest-0.4.x` + * `$path_user`: + * Linux: `$HOME/.minetest` + * Windows: `C:/users//AppData/minetest` (maybe) + +Mod load path +------------- +Generic: + +* `$path_share/clientmods/` +* `$path_user/clientmods/` (User-installed mods) + +In a run-in-place version (e.g. the distributed windows version): + +* `minetest-0.4.x/clientmods/` (User-installed mods) + +On an installed version on Linux: + +* `/usr/share/minetest/clientmods/` +* `$HOME/.minetest/clientmods/` (User-installed mods) + +Modpack support +---------------- + +Mods can be put in a subdirectory, if the parent directory, which otherwise +should be a mod, contains a file named `modpack.conf`. +The file is a key-value store of modpack details. + +* `name`: The modpack name. +* `description`: Description of mod to be shown in the Mods tab of the main + menu. + +Mod directory structure +------------------------ + + clientmods + ├── modname + │   ├── mod.conf + │   ├── init.lua + └── another + +### modname + +The location of this directory. + +### mod.conf + +An (optional) settings file that provides meta information about the mod. + +* `name`: The mod name. Allows Minetest to determine the mod name even if the + folder is wrongly named. +* `description`: Description of mod to be shown in the Mods tab of the main + menu. +* `depends`: A comma separated list of dependencies. These are mods that must be + loaded before this mod. +* `optional_depends`: A comma separated list of optional dependencies. + Like a dependency, but no error if the mod doesn't exist. + +### `init.lua` + +The main Lua script. Running this script should register everything it +wants to register. Subsequent execution depends on minetest calling the +registered callbacks. + +**NOTE**: Client mods currently can't provide textures, sounds, or models by +themselves. Any media referenced in function calls must already be loaded +(provided by mods that exist on the server). + +Naming convention for registered textual names +---------------------------------------------- +Registered names should generally be in this format: + + "modname:" ( can have characters a-zA-Z0-9_) + +This is to prevent conflicting names from corrupting maps and is +enforced by the mod loader. + +### Example +In the mod `experimental`, there is the ideal item/node/entity name `tnt`. +So the name should be `experimental:tnt`. + +Enforcement can be overridden by prefixing the name with `:`. This can +be used for overriding the registrations of some other mod. + +Example: Any mod can redefine `experimental:tnt` by using the name + + :experimental:tnt + +when registering it. +(also that mod is required to have `experimental` as a dependency) + +The `:` prefix can also be used for maintaining backwards compatibility. + +Sounds +------ +**NOTE: Connecting sounds to objects is not implemented.** + +Only Ogg Vorbis files are supported. + +For positional playing of sounds, only single-channel (mono) files are +supported. Otherwise OpenAL will play them non-positionally. + +Mods should generally prefix their sounds with `modname_`, e.g. given +the mod name "`foomod`", a sound could be called: + + foomod_foosound.ogg + +Sounds are referred to by their name with a dot, a single digit and the +file extension stripped out. When a sound is played, the actual sound file +is chosen randomly from the matching sounds. + +When playing the sound `foomod_foosound`, the sound is chosen randomly +from the available ones of the following files: + +* `foomod_foosound.ogg` +* `foomod_foosound.0.ogg` +* `foomod_foosound.1.ogg` +* (...) +* `foomod_foosound.9.ogg` + +Examples of sound parameter tables: + + -- Play locationless + { + gain = 1.0, -- default + } + -- Play locationless, looped + { + gain = 1.0, -- default + loop = true, + } + -- Play in a location + { + pos = {x = 1, y = 2, z = 3}, + gain = 1.0, -- default + } + -- Play connected to an object, looped + { + object = , + gain = 1.0, -- default + loop = true, + } + +Looped sounds must either be connected to an object or played locationless. + +### SimpleSoundSpec +* e.g. `""` +* e.g. `"default_place_node"` +* e.g. `{}` +* e.g. `{name = "default_place_node"}` +* e.g. `{name = "default_place_node", gain = 1.0}` + +Representations of simple things +-------------------------------- + +### Position/vector + + {x=num, y=num, z=num} + +For helper functions see "Vector helpers". + +### pointed_thing +* `{type="nothing"}` +* `{type="node", under=pos, above=pos}` +* `{type="object", id=ObjectID}` + +Flag Specifier Format +--------------------- +Flags using the standardized flag specifier format can be specified in either of +two ways, by string or table. + +The string format is a comma-delimited set of flag names; whitespace and +unrecognized flag fields are ignored. Specifying a flag in the string sets the +flag, and specifying a flag prefixed by the string `"no"` explicitly +clears the flag from whatever the default may be. + +In addition to the standard string flag format, the schematic flags field can +also be a table of flag names to boolean values representing whether or not the +flag is set. Additionally, if a field with the flag name prefixed with `"no"` +is present, mapped to a boolean of any value, the specified flag is unset. + +E.g. A flag field of value + + {place_center_x = true, place_center_y=false, place_center_z=true} + +is equivalent to + + {place_center_x = true, noplace_center_y=true, place_center_z=true} + +which is equivalent to + + "place_center_x, noplace_center_y, place_center_z" + +or even + + "place_center_x, place_center_z" + +since, by default, no schematic attributes are set. + +Formspec +-------- +Formspec defines a menu. It is a string, with a somewhat strange format. + +Spaces and newlines can be inserted between the blocks, as is used in the +examples. + +### Examples + +#### Chest + + size[8,9] + list[context;main;0,0;8,4;] + list[current_player;main;0,5;8,4;] + +#### Furnace + + size[8,9] + list[context;fuel;2,3;1,1;] + list[context;src;2,1;1,1;] + list[context;dst;5,1;2,2;] + list[current_player;main;0,5;8,4;] + +#### Minecraft-like player inventory + + size[8,7.5] + image[1,0.6;1,2;player.png] + list[current_player;main;0,3.5;8,4;] + list[current_player;craft;3,0;3,3;] + list[current_player;craftpreview;7,1;1,1;] + +### Elements + +#### `size[,,]` +* Define the size of the menu in inventory slots +* `fixed_size`: `true`/`false` (optional) +* deprecated: `invsize[,;]` + +#### `container[,]` +* Start of a container block, moves all physical elements in the container by (X, Y) +* Must have matching container_end +* Containers can be nested, in which case the offsets are added + (child containers are relative to parent containers) + +#### `container_end[]` +* End of a container, following elements are no longer relative to this container + +#### `list[;;,;,;]` +* Show an inventory list + +#### `list[;;,;,;]` +* Show an inventory list + +#### `listring[;]` +* Allows to create a ring of inventory lists +* Shift-clicking on items in one element of the ring + will send them to the next inventory list inside the ring +* The first occurrence of an element inside the ring will + determine the inventory where items will be sent to + +#### `listring[]` +* Shorthand for doing `listring[;]` + for the last two inventory lists added by list[...] + +#### `listcolors[;]` +* Sets background color of slots as `ColorString` +* Sets background color of slots on mouse hovering + +#### `listcolors[;;]` +* Sets background color of slots as `ColorString` +* Sets background color of slots on mouse hovering +* Sets color of slots border + +#### `listcolors[;;;;]` +* Sets background color of slots as `ColorString` +* Sets background color of slots on mouse hovering +* Sets color of slots border +* Sets default background color of tooltips +* Sets default font color of tooltips + +#### `tooltip[;;,]` +* Adds tooltip for an element +* `` tooltip background color as `ColorString` (optional) +* `` tooltip font color as `ColorString` (optional) + +#### `image[,;,;]` +* Show an image +* Position and size units are inventory slots + +#### `item_image[,;,;]` +* Show an inventory image of registered item/node +* Position and size units are inventory slots + +#### `bgcolor[;]` +* Sets background color of formspec as `ColorString` +* If `true`, the background color is drawn fullscreen (does not effect the size of the formspec) + +#### `background[,;,;]` +* Use a background. Inventory rectangles are not drawn then. +* Position and size units are inventory slots +* Example for formspec 8x4 in 16x resolution: image shall be sized + 8 times 16px times 4 times 16px. + +#### `background[,;,;;]` +* Use a background. Inventory rectangles are not drawn then. +* Position and size units are inventory slots +* Example for formspec 8x4 in 16x resolution: + image shall be sized 8 times 16px times 4 times 16px +* If `true` the background is clipped to formspec size + (`x` and `y` are used as offset values, `w` and `h` are ignored) + +#### `pwdfield[,;,;;