diff --git a/christmastree/Changelog.txt b/christmastree/Changelog.txt new file mode 100644 index 0000000..bbaf116 --- /dev/null +++ b/christmastree/Changelog.txt @@ -0,0 +1,10 @@ +1.0.2 + Added two larger outdoor decorated Christmas trees. One version has snow. + +1.0.1 + Added Christmas wreath model and UV + Reduced image sizes and removed high res images + Fixed collision boxes and groups. Can't walk through trees now. + +1.0 + initial release. diff --git a/christmastree/README.txt b/christmastree/README.txt new file mode 100644 index 0000000..e30f0ad --- /dev/null +++ b/christmastree/README.txt @@ -0,0 +1,68 @@ +Minetest mod "christmastree" +=================== +by ExeterDad (c) 2014 + +Version: 1.0.1 + +About +~~~~~ +This mod adds a decorated Christmas trees and a wreath to Minetest. UV mapped textures +have greenery, baubles candy canes, star topper, tree skirt, and wrapped gifts. +The Christmas trees and wreath emit light at level 12 so not to be dark inside a building. +A Christmas wreath is included. +Larger 5 block high Christmas trees, with and without snow. +UV textures are 256x256 + + + +To craft a indoor Christmas tree: + +0 + 0 +0 X 0 +X X X + +0 = Blank ++ = default:gold_ingot +X = default:sapling + +To craft a outdoor Christmas tree: + +0 + 0 +0 X 0 +X X X + +0 = Blank ++ = default:gold_ingot +X = (2)default:sapling + +To craft a outdoor Christmas tree with snow: + +0 + 0 +o X o +X X X + +0 = Blank ++ = default:gold_ingot +o = default:snow +X = (2)default:sapling + +To craft a Christmas wreath: + +0 X 0 +X 0 X +0 X 0 + +0 = Blank +X = default:sapling + +License of source code, obj model, textures: WTFPL +--------------------------------------- +(c) Copyright ExeterDad (2014) + + + +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://sam.zoy.org/wtfpl/COPYING for more details. diff --git a/christmastree/depends.txt b/christmastree/depends.txt new file mode 100644 index 0000000..4ad96d5 --- /dev/null +++ b/christmastree/depends.txt @@ -0,0 +1 @@ +default diff --git a/christmastree/init.lua b/christmastree/init.lua new file mode 100644 index 0000000..89ead8e --- /dev/null +++ b/christmastree/init.lua @@ -0,0 +1,107 @@ + +local treebox = { + type ="fixed", + fixed = { { -0.8, -0.5, -0.8, 0.8, 2.5, 0.8 } }, +} + +local wreathbox = { + type ="fixed", + fixed = { { -0.5, -0.5, 0.33, 0.5, 0.5, 0.5 } }, +} + +local outdoortreebox = { + type ="fixed", + fixed = { { -1.448, -0.5, -1.448, 1.448, 4.5, 1.448 } }, +} + +minetest.register_node("christmastree:indoortree", { + description = "Indoor decorated Christmas Tree", + drawtype = "mesh", + paramtype = "light", + light_source = 12, + paramtype2 = "facedir", + mesh = "indoor-christmas-tree.obj", + tiles = { "indoor-tree_UV256.png" }, + groups = {choppy=2, oddly_breakable_by_hand=2, flammable=3}, + sounds = default.node_sound_wood_defaults(), + selection_box = treebox, + collision_box = treebox, +}) + +minetest.register_node("christmastree:outdoortree", { + description = "Outdoor decorated Christmas Tree", + drawtype = "mesh", + paramtype = "light", + light_source = 12, + paramtype2 = "facedir", + mesh = "outdoor-christmas-tree.obj", + tiles = { "christmastree_outdoor256.png" }, + groups = {choppy=2, oddly_breakable_by_hand=2, flammable=3}, + sounds = default.node_sound_wood_defaults(), + selection_box = outdoortreebox, + collision_box = outdoortreebox, +}) + +minetest.register_node("christmastree:outdoortree_snow", { + description = "Outdoor decorated Christmas Tree with snow", + drawtype = "mesh", + paramtype = "light", + light_source = 12, + paramtype2 = "facedir", + mesh = "outdoor-christmas-tree.obj", + tiles = { "christmastree_outdoor_snow256.png" }, + groups = {choppy=2, oddly_breakable_by_hand=2, flammable=3}, + sounds = default.node_sound_wood_defaults(), + selection_box = outdoortreebox, + collision_box = outdoortreebox, +}) + +minetest.register_node("christmastree:christmas_wreath", { + description = "Christmas Wreath", + drawtype = "mesh", + paramtype = "light", + light_source = 12, + paramtype2 = "facedir", + mesh = "christmas-wreath.obj", + tiles = { "wreath_UV256.png" }, + groups = {choppy=2, oddly_breakable_by_hand=2, flammable=3}, + sounds = default.node_sound_wood_defaults(), + selection_box = wreathbox, + collision_box = wreathbox, +}) + +minetest.register_craft({ + output = "christmastree:indoortree", + recipe = { + { "","default:gold_ingot","" }, + { "","default:sapling","" }, + { "default:sapling","default:sapling","default:sapling" }, + }, +}) + +minetest.register_craft({ + output = "christmastree:outdoortree", + recipe = { + { "","default:gold_ingot","" }, + { "","default:sapling 2","" }, + { "default:sapling 2","default:sapling 2","default:sapling 2" }, + }, +}) + +minetest.register_craft({ + output = "christmastree:outdoortree_snow", + recipe = { + { "","default:gold_ingot","" }, + { "default:snow","default:sapling 2","default:snow" }, + { "default:sapling 2","default:sapling 2","default:sapling 2" }, + }, +}) + +minetest.register_craft({ + output = "christmastree:christmas_wreath", + recipe = { + { "","default:sapling","" }, + { "default:sapling","","default:sapling" }, + { "","default:sapling","" }, + }, +}) diff --git a/christmastree/mod.conf b/christmastree/mod.conf new file mode 100644 index 0000000..f2c8902 --- /dev/null +++ b/christmastree/mod.conf @@ -0,0 +1,5 @@ +release = 8099 +author = ExeterDad +name = christmastree +description = Adds a decorated indoor Christmas tree with wrapped gifts. +title = Christmas Tree diff --git a/christmastree/models/christmas-wreath.obj b/christmastree/models/christmas-wreath.obj new file mode 100644 index 0000000..f4b5f40 --- /dev/null +++ b/christmastree/models/christmas-wreath.obj @@ -0,0 +1,827 @@ +# Blender v2.72 (sub 0) OBJ File: 'christmas_wreath.blend' +# www.blender.org +v -0.014074 0.350084 0.370711 +v -0.002834 0.325585 0.372605 +v 0.019802 0.325585 0.373064 +v 0.039062 0.350084 0.371876 +v 0.019832 0.374582 0.370366 +v -0.002758 0.374582 0.369119 +v 0.239043 0.350084 0.359450 +v 0.227748 0.280124 0.362253 +v 0.205157 0.254010 0.366836 +v 0.193862 0.350084 0.368161 +v 0.205157 0.446157 0.365737 +v 0.227748 0.420043 0.361280 +v 0.239043 0.350084 0.387609 +v 0.227748 0.280124 0.384806 +v 0.205157 0.254010 0.380223 +v 0.193862 0.350084 0.377865 +v 0.205157 0.446157 0.381322 +v 0.227748 0.420043 0.385779 +v -0.014036 0.350084 0.378681 +v 0.019832 0.374582 0.376709 +v -0.002758 0.374582 0.378137 +v 0.123291 0.257205 0.380716 +v 0.124349 0.350084 0.373108 +v 0.123291 0.442962 0.383917 +v 0.121175 0.442962 0.380141 +v 0.121175 0.257205 0.378342 +v 0.118981 0.258217 0.370002 +v 0.120562 0.441950 0.363243 +v 0.118981 0.441950 0.366974 +v 0.120562 0.258217 0.367495 +v -0.036266 0.339779 0.372179 +v -0.025049 0.315280 0.374348 +v -0.036185 0.339779 0.378839 +v -0.024902 0.315280 0.378658 +v -0.099209 0.262727 0.384096 +v -0.029312 0.259746 0.388242 +v -0.099506 0.262727 0.389415 +v -0.029567 0.259746 0.400862 +v -0.128154 0.104509 0.377590 +v -0.052302 0.099821 0.402511 +v -0.130728 0.104509 0.385644 +v -0.054483 0.099821 0.409334 +v -0.155930 -0.053713 0.367841 +v -0.065178 -0.059321 0.397657 +v -0.159010 -0.053713 0.377477 +v -0.067787 -0.059321 0.405821 +v -0.173854 -0.211003 0.355902 +v -0.070674 -0.217073 0.364260 +v -0.173068 -0.211003 0.367003 +v -0.070008 -0.217073 0.373672 +v -0.193260 -0.364070 0.357167 +v -0.089956 -0.360737 0.350581 +v -0.193366 -0.364424 0.368088 +v -0.090090 -0.360359 0.359834 +v -0.185556 -0.501254 0.420498 +v -0.087828 -0.467472 0.413913 +v -0.184829 -0.500954 0.431419 +v -0.087212 -0.467218 0.423165 +v -0.089329 -0.461048 0.423138 +v -0.186946 -0.494784 0.431392 +v -0.187673 -0.495084 0.420471 +v -0.089945 -0.461302 0.413885 +v -0.478061 0.042207 0.499029 +v -0.458491 0.042207 0.425992 +v -0.405024 0.042207 0.372525 +v -0.258950 0.042207 0.400326 +v -0.205483 0.042207 0.426935 +v -0.185913 0.042207 0.478185 +v -0.386760 0.324364 0.499029 +v -0.370927 0.312813 0.425992 +v -0.327671 0.281256 0.372525 +v -0.209495 0.195042 0.400326 +v -0.166239 0.163485 0.426935 +v -0.150406 0.151935 0.478185 +v -0.147729 0.498746 0.499029 +v -0.141682 0.480057 0.425992 +v -0.125159 0.428997 0.372525 +v -0.080020 0.289499 0.400326 +v -0.063498 0.238439 0.426935 +v -0.057450 0.219750 0.478185 +v 0.147729 0.498746 0.499029 +v 0.141681 0.480057 0.425992 +v 0.125159 0.428997 0.372525 +v 0.080020 0.289499 0.400326 +v 0.063498 0.238439 0.426935 +v 0.057450 0.219750 0.478185 +v 0.386760 0.324363 0.499029 +v 0.370927 0.312813 0.425992 +v 0.327671 0.281256 0.372525 +v 0.209495 0.195042 0.400326 +v 0.166239 0.163485 0.426935 +v 0.150406 0.151935 0.478185 +v 0.478061 0.042207 0.499029 +v 0.458491 0.042207 0.425992 +v 0.405024 0.042207 0.372525 +v 0.258950 0.042207 0.400326 +v 0.205483 0.042207 0.426935 +v 0.185913 0.042207 0.478185 +v 0.386760 -0.239950 0.499029 +v 0.370927 -0.228399 0.425992 +v 0.327671 -0.196842 0.372525 +v 0.209495 -0.110628 0.400326 +v 0.166239 -0.079071 0.426935 +v 0.150406 -0.067521 0.478185 +v 0.147729 -0.414332 0.499029 +v 0.141681 -0.395643 0.425992 +v 0.125159 -0.344583 0.372525 +v 0.080020 -0.205085 0.400326 +v 0.063498 -0.154025 0.426935 +v 0.057450 -0.135336 0.478185 +v -0.147729 -0.414332 0.499029 +v -0.141682 -0.395643 0.425992 +v -0.125159 -0.344583 0.372525 +v -0.080020 -0.205085 0.400326 +v -0.063498 -0.154025 0.426935 +v -0.057450 -0.135336 0.478185 +v -0.386759 -0.239950 0.499029 +v -0.370927 -0.228399 0.425992 +v -0.327671 -0.196843 0.372525 +v -0.209495 -0.110628 0.400326 +v -0.166239 -0.079071 0.426935 +v -0.150406 -0.067521 0.478185 +v -0.000000 0.295699 0.384408 +v -0.031591 0.304008 0.382767 +v -0.031591 0.320625 0.379486 +v -0.000000 0.328934 0.377845 +v 0.031591 0.320625 0.379486 +v 0.031591 0.304008 0.382767 +v -0.000000 0.404468 0.384408 +v -0.031591 0.396159 0.382767 +v -0.031591 0.379542 0.379486 +v -0.000000 0.371233 0.377845 +v 0.031591 0.379542 0.379486 +v 0.031591 0.396159 0.382767 +v 0.000000 0.404468 0.362930 +v -0.031591 0.396159 0.364570 +v -0.031591 0.379542 0.367852 +v 0.000000 0.371233 0.369493 +v 0.031591 0.379542 0.367852 +v 0.031591 0.396159 0.364570 +v -0.000000 0.304166 0.366121 +v -0.031591 0.312474 0.367762 +v -0.031591 0.320625 0.367852 +v -0.000000 0.328934 0.369493 +v 0.031591 0.320625 0.367852 +v 0.031591 0.312474 0.367762 +v 0.008014 0.355736 0.383473 +v -0.000028 0.329754 0.378384 +v -0.021084 0.327725 0.377224 +v -0.039724 0.350399 0.377497 +v -0.022648 0.374483 0.379205 +v -0.002631 0.376740 0.382084 +v -0.244600 0.334099 0.381311 +v -0.227239 0.265383 0.379068 +v -0.202300 0.241331 0.375602 +v -0.199190 0.338030 0.374362 +v -0.218790 0.432763 0.376700 +v -0.239255 0.404781 0.380040 +v -0.243219 0.334219 0.353186 +v -0.226133 0.265479 0.356541 +v -0.201644 0.241388 0.362231 +v -0.198710 0.338072 0.364583 +v -0.218026 0.432830 0.361134 +v -0.238053 0.404885 0.355570 +v 0.008140 0.355978 0.375758 +v 0.000325 0.329785 0.373066 +v -0.020973 0.327731 0.373132 +v -0.039701 0.350399 0.373342 +v -0.022573 0.374492 0.372993 +v -0.002480 0.376850 0.373303 +v -0.121279 0.251552 0.366333 +v -0.136160 0.436709 0.362571 +v -0.134240 0.436876 0.366447 +v -0.119373 0.251717 0.368919 +v -0.117725 0.252875 0.377419 +v -0.134372 0.435848 0.383355 +v -0.132616 0.436000 0.379706 +v -0.119358 0.252734 0.379739 +v 0.032560 0.345623 0.382119 +v 0.022723 0.319938 0.376727 +v 0.032755 0.345719 0.375716 +v 0.023193 0.319919 0.372577 +v 0.090470 0.262727 0.387950 +v 0.022828 0.259746 0.369541 +v 0.091833 0.262727 0.382762 +v 0.023993 0.259746 0.365146 +v 0.098859 0.104509 0.397698 +v 0.023537 0.099821 0.370940 +v 0.101527 0.104509 0.389736 +v 0.025867 0.099821 0.364262 +v 0.147224 -0.053713 0.396329 +v 0.053068 -0.059321 0.379551 +v 0.148885 -0.053713 0.386368 +v 0.054530 -0.059321 0.371106 +v 0.200195 -0.211264 0.373591 +v 0.096757 -0.217355 0.372983 +v 0.200200 -0.211583 0.362793 +v 0.096773 -0.217624 0.363837 +v 0.235612 -0.370766 0.348349 +v 0.132307 -0.365155 0.349706 +v 0.235247 -0.374659 0.338139 +v 0.131954 -0.368437 0.341068 +v 0.232748 -0.492415 0.395455 +v 0.132722 -0.465608 0.397787 +v 0.231038 -0.497801 0.386043 +v 0.131328 -0.469941 0.389815 +v 0.132718 -0.464478 0.386542 +v 0.232428 -0.492339 0.382770 +v 0.234124 -0.486942 0.392192 +v 0.134101 -0.460139 0.394521 +vt 0.630783 0.463337 +vt 0.664081 0.440474 +vt 0.660184 0.452608 +vt 0.630283 0.463561 +vt 0.604018 0.401001 +vt 0.632748 0.387180 +vt 0.644100 0.392830 +vt 0.604499 0.400779 +vt 0.913308 0.919672 +vt 0.902899 0.919672 +vt 0.903765 0.894055 +vt 0.912102 0.894055 +vt 0.905182 0.884814 +vt 0.910130 0.884814 +vt 0.973239 0.452597 +vt 0.968344 0.452327 +vt 0.968380 0.421463 +vt 0.971929 0.421658 +vt 0.967540 0.387180 +vt 0.973239 0.387494 +vt 0.910641 0.955715 +vt 0.904880 0.955715 +vt 0.903482 0.945684 +vt 0.912538 0.945684 +vt 0.242518 0.891615 +vt 0.217324 0.854009 +vt 0.217311 0.845658 +vt 0.242501 0.892455 +vt 0.452558 0.752973 +vt 0.419275 0.711573 +vt 0.418938 0.701608 +vt 0.465948 0.721515 +vt 0.511346 0.712543 +vt 0.478969 0.689725 +vt 0.478203 0.689424 +vt 0.507289 0.700409 +vt 0.479927 0.765836 +vt 0.451765 0.752674 +vt 0.491425 0.760186 +vt 0.505276 0.737985 +vt 0.571868 0.451409 +vt 0.572202 0.441442 +vt 0.657901 0.415031 +vt 0.579222 0.458133 +vt 0.066618 0.847241 +vt 0.091340 0.892869 +vt 0.091362 0.892326 +vt 0.066615 0.855636 +vt 0.023686 0.892292 +vt 0.048551 0.855606 +vt 0.057579 0.862754 +vt 0.092857 0.924203 +vt 0.057498 0.920050 +vt 0.022100 0.924203 +vt 0.023653 0.892815 +vt 0.048547 0.847214 +vt 0.337368 0.727613 +vt 0.342606 0.692091 +vt 0.344567 0.692018 +vt 0.339803 0.727590 +vt 0.573153 0.464833 +vt 0.579841 0.484337 +vt 0.555546 0.493378 +vt 0.565803 0.458107 +vt 0.909444 0.387180 +vt 0.911035 0.387212 +vt 0.916000 0.407719 +vt 0.914279 0.407691 +vt 0.414336 0.601197 +vt 0.427099 0.543266 +vt 0.459499 0.554430 +vt 0.441417 0.610528 +vt 0.921365 0.466405 +vt 0.918817 0.466395 +vt 0.595053 0.542079 +vt 0.568164 0.551425 +vt 0.337495 0.634321 +vt 0.340560 0.633893 +vt 0.613301 0.598128 +vt 0.581131 0.609309 +vt 0.331144 0.576742 +vt 0.334811 0.576230 +vt 0.919221 0.524865 +vt 0.916173 0.524853 +vt 0.906369 0.582804 +vt 0.902899 0.582728 +vt 0.597286 0.665463 +vt 0.633423 0.652683 +vt 0.647591 0.704354 +vt 0.612985 0.720333 +vt 0.444528 0.487701 +vt 0.480610 0.500443 +vt 0.323570 0.520963 +vt 0.327600 0.521022 +vt 0.509746 0.414513 +vt 0.481475 0.388976 +vt 0.483075 0.387180 +vt 0.511346 0.412716 +vt 0.331561 0.466167 +vt 0.335556 0.465974 +vt 0.911244 0.635425 +vt 0.907821 0.635503 +vt 0.460053 0.432495 +vt 0.494479 0.448741 +vt 0.854273 0.934191 +vt 0.858296 0.934389 +vt 0.858296 0.967730 +vt 0.854887 0.967563 +vt 0.634117 0.764093 +vt 0.662479 0.738547 +vt 0.664081 0.740344 +vt 0.635719 0.765890 +vt 0.344567 0.422693 +vt 0.340593 0.422541 +vt 0.340350 0.420772 +vt 0.344324 0.420924 +vt 0.919927 0.672844 +vt 0.923341 0.672825 +vt 0.923287 0.675138 +vt 0.919873 0.675157 +vt 0.426795 0.668592 +vt 0.402491 0.659556 +vt 0.323570 0.970242 +vt 0.323570 0.939008 +vt 0.324770 0.944714 +vt 0.324770 0.964683 +vt 0.473901 0.960501 +vt 0.473901 0.938761 +vt 0.475114 0.932631 +vt 0.475114 0.966635 +vt 0.975446 0.507877 +vt 0.967540 0.507877 +vt 0.967645 0.496928 +vt 0.974343 0.496928 +vt 0.330222 0.939416 +vt 0.329022 0.944975 +vt 0.972316 0.809978 +vt 0.968022 0.809926 +vt 0.968820 0.797844 +vt 0.971903 0.797882 +vt 0.968022 0.786933 +vt 0.972316 0.786986 +vt 0.469600 0.938755 +vt 0.468388 0.932621 +vt 0.974760 0.519960 +vt 0.968062 0.519960 +vt 0.915863 0.329379 +vt 0.915863 0.289203 +vt 0.927512 0.292258 +vt 0.927512 0.326296 +vt 0.330222 0.970650 +vt 0.329022 0.964944 +vt 0.623086 0.932580 +vt 0.644779 0.932556 +vt 0.641734 0.944293 +vt 0.626160 0.944310 +vt 0.644779 0.955967 +vt 0.623086 0.955991 +vt 0.469600 0.960495 +vt 0.468388 0.966624 +vt 0.904165 0.326308 +vt 0.904165 0.292270 +vt 0.975468 0.576382 +vt 0.967540 0.576382 +vt 0.968191 0.564291 +vt 0.974908 0.564291 +vt 0.968022 0.719488 +vt 0.972303 0.719567 +vt 0.972000 0.730511 +vt 0.968926 0.730455 +vt 0.972303 0.742602 +vt 0.968022 0.742524 +vt 0.975188 0.587282 +vt 0.968471 0.587282 +vt 0.029602 0.733011 +vt 0.023344 0.695786 +vt 0.046547 0.706027 +vt 0.036988 0.726041 +vt 0.245696 0.496712 +vt 0.279371 0.474815 +vt 0.275080 0.486813 +vt 0.245159 0.496897 +vt 0.220142 0.433886 +vt 0.249249 0.420772 +vt 0.260562 0.426726 +vt 0.220707 0.433705 +vt 0.967538 0.192508 +vt 0.977900 0.192763 +vt 0.977900 0.213498 +vt 0.969601 0.213293 +vt 0.181286 0.977834 +vt 0.172955 0.977834 +vt 0.174980 0.967090 +vt 0.179925 0.967090 +vt 0.957864 0.884814 +vt 0.962814 0.884814 +vt 0.962101 0.920440 +vt 0.958480 0.920440 +vt 0.963270 0.955740 +vt 0.957507 0.955740 +vt 0.970199 0.155370 +vt 0.975934 0.155511 +vt 0.977002 0.168014 +vt 0.967987 0.167792 +vt 0.556717 0.858165 +vt 0.581542 0.810681 +vt 0.581455 0.818608 +vt 0.556692 0.857414 +vt 0.625106 0.857076 +vt 0.598660 0.817719 +vt 0.598893 0.810221 +vt 0.625144 0.857854 +vt 0.971177 0.038208 +vt 0.969212 0.038092 +vt 0.969465 0.030574 +vt 0.970976 0.030607 +vt 0.969748 0.022166 +vt 0.971282 0.022167 +vt 0.273493 0.967090 +vt 0.275016 0.967096 +vt 0.275265 0.977789 +vt 0.272986 0.977789 +vt 0.120285 0.975807 +vt 0.118000 0.975728 +vt 0.117059 0.968534 +vt 0.120285 0.968636 +vt 0.069618 0.968534 +vt 0.072860 0.968737 +vt 0.072860 0.976683 +vt 0.070014 0.976452 +vt 0.110157 0.796931 +vt 0.071099 0.790831 +vt 0.036288 0.749100 +vt 0.036113 0.740420 +vt 0.042978 0.733031 +vt 0.096090 0.726938 +vt 0.128756 0.748729 +vt 0.123339 0.774352 +vt 0.095401 0.726655 +vt 0.124462 0.736689 +vt 0.626298 0.888225 +vt 0.590897 0.884040 +vt 0.555546 0.888225 +vt 0.590354 0.824800 +vt 0.098922 0.802883 +vt 0.070391 0.790532 +vt 0.447199 0.810751 +vt 0.471701 0.857240 +vt 0.471687 0.856666 +vt 0.447272 0.818607 +vt 0.430078 0.817673 +vt 0.404040 0.856322 +vt 0.404062 0.856923 +vt 0.429899 0.810168 +vt 0.967590 0.649438 +vt 0.855832 0.039309 +vt 0.853898 0.039351 +vt 0.969929 0.649177 +vt 0.187687 0.497737 +vt 0.197569 0.517563 +vt 0.174467 0.527959 +vt 0.180170 0.490934 +vt 0.827219 0.889860 +vt 0.825675 0.889839 +vt 0.822790 0.869411 +vt 0.824462 0.869329 +vt 0.822035 0.815463 +vt 0.824606 0.815257 +vt 0.223450 0.570626 +vt 0.197431 0.581847 +vt 0.859966 0.097801 +vt 0.856993 0.097876 +vt 0.239575 0.628271 +vt 0.207280 0.642123 +vt 0.045659 0.641641 +vt 0.071823 0.652673 +vt 0.859532 0.157036 +vt 0.855831 0.157099 +vt 0.822624 0.765850 +vt 0.825793 0.765807 +vt 0.817304 0.718221 +vt 0.820649 0.718495 +vt 0.215736 0.702603 +vt 0.251001 0.687638 +vt 0.263641 0.742023 +vt 0.230354 0.760809 +vt 0.087992 0.595135 +vt 0.055588 0.581476 +vt 0.064372 0.521001 +vt 0.099676 0.535818 +vt 0.851144 0.216178 +vt 0.847153 0.216332 +vt 0.127447 0.447823 +vt 0.098897 0.422393 +vt 0.100206 0.420772 +vt 0.128756 0.446202 +vt 0.842039 0.275739 +vt 0.838285 0.277204 +vt 0.806721 0.671787 +vt 0.809867 0.673125 +vt 0.079414 0.461458 +vt 0.112666 0.480301 +vt 0.810073 0.972489 +vt 0.806721 0.972619 +vt 0.807229 0.934298 +vt 0.810073 0.934191 +vt 0.249332 0.799690 +vt 0.278057 0.774445 +vt 0.279371 0.776079 +vt 0.250645 0.801326 +vt 0.855281 0.320421 +vt 0.858723 0.318427 +vt 0.859966 0.320411 +vt 0.856528 0.322401 +vt 0.825960 0.642099 +vt 0.823109 0.640265 +vt 0.824372 0.638277 +vt 0.827219 0.640110 +vt 0.713099 0.590929 +vt 0.708280 0.481970 +vt 0.736047 0.482672 +vt 0.740669 0.587170 +vt 0.753518 0.492220 +vt 0.762522 0.578535 +vt 0.591864 0.262426 +vt 0.635044 0.221853 +vt 0.682302 0.247535 +vt 0.614763 0.310995 +vt 0.510472 0.088702 +vt 0.569197 0.081308 +vt 0.565529 0.101181 +vt 0.518929 0.107049 +vt 0.922880 0.200806 +vt 0.923339 0.244872 +vt 0.904581 0.240305 +vt 0.904165 0.200436 +vt 0.713099 0.387180 +vt 0.740669 0.391762 +vt 0.757439 0.407044 +vt 0.487458 0.039274 +vt 0.574389 0.033706 +vt 0.621035 0.109916 +vt 0.606664 0.123883 +vt 0.904639 0.840483 +vt 0.902899 0.808440 +vt 0.922248 0.805138 +vt 0.923822 0.834129 +vt 0.810010 0.593945 +vt 0.806721 0.486275 +vt 0.834309 0.486859 +vt 0.837463 0.590121 +vt 0.852007 0.489124 +vt 0.853617 0.581474 +vt 0.421457 0.099657 +vt 0.467291 0.129275 +vt 0.456149 0.187528 +vt 0.402491 0.193847 +vt 0.646187 0.163600 +vt 0.626622 0.166482 +vt 0.902899 0.762225 +vt 0.922248 0.763324 +vt 0.810010 0.387180 +vt 0.837463 0.391820 +vt 0.858700 0.405168 +vt 0.617780 0.212707 +vt 0.904639 0.719488 +vt 0.923822 0.724658 +vt 0.736116 0.716770 +vt 0.735048 0.822478 +vt 0.708524 0.810133 +vt 0.709304 0.720715 +vt 0.699730 0.257920 +vt 0.623520 0.317793 +vt 0.481300 0.241212 +vt 0.441831 0.277814 +vt 0.583516 0.244902 +vt 0.327439 0.894677 +vt 0.323570 0.859435 +vt 0.343301 0.856794 +vt 0.346802 0.888679 +vt 0.736116 0.924997 +vt 0.710842 0.912801 +vt 0.520837 0.335232 +vt 0.522912 0.322560 +vt 0.695824 0.156421 +vt 0.655308 0.073585 +vt 0.533139 0.269820 +vt 0.536916 0.250770 +vt 0.323570 0.812556 +vt 0.343301 0.814380 +vt 0.751536 0.303608 +vt 0.746032 0.229154 +vt 0.773355 0.225412 +vt 0.780172 0.289201 +vt 0.427514 0.292195 +vt 0.495781 0.228069 +vt 0.327439 0.771945 +vt 0.346802 0.777636 +vt 0.743930 0.121653 +vt 0.771339 0.122312 +vt 0.792304 0.124512 +vt 0.794086 0.215590 +vt 0.475822 0.185469 +vt 0.905579 0.111039 +vt 0.904165 0.064759 +vt 0.923733 0.065443 +vt 0.925012 0.107315 +vt 0.746032 0.022166 +vt 0.773355 0.026897 +vt 0.795624 0.037148 +vt 0.484664 0.139244 +vt 0.905579 0.022166 +vt 0.925012 0.026906 +vt 0.725714 0.672438 +vt 0.752768 0.665343 +vt 0.402710 0.094002 +vt 0.479165 0.022166 +vt 0.923339 0.155370 +vt 0.904581 0.159327 +vt 0.255750 0.220874 +vt 0.358292 0.253857 +vt 0.294085 0.342496 +vt 0.230781 0.255345 +vt 0.358292 0.144380 +vt 0.255750 0.178300 +vt 0.294085 0.055882 +vt 0.230781 0.143884 +vt 0.190379 0.130772 +vt 0.190196 0.022166 +vt 0.149978 0.143973 +vt 0.086307 0.056110 +vt 0.125008 0.178444 +vt 0.022100 0.144749 +vt 0.022100 0.254226 +vt 0.125008 0.221018 +vt 0.149978 0.255434 +vt 0.086307 0.342725 +vt 0.190379 0.268546 +vt 0.190196 0.376441 +s 1 +f 30/1 8/2 9/3 27/4 +f 29/5 11/6 12/7 28/8 +f 7/9 13/10 14/11 8/12 +f 8/12 14/11 15/13 9/14 +f 9/15 15/16 16/17 10/18 +f 10/18 16/17 17/19 11/20 +f 11/21 17/22 18/23 12/24 +f 12/24 18/23 13/10 7/9 +f 25/25 20/26 21/27 24/28 +f 24/29 21/30 19/31 23/32 +f 14/33 22/34 26/35 15/36 +f 17/37 25/38 24/29 18/39 +f 18/39 24/29 23/32 13/40 +f 1/41 6/42 28/8 12/7 7/43 8/2 30/1 2/44 +f 2/45 30/46 27/47 3/48 +f 29/49 5/50 4/51 3/48 27/47 9/52 10/53 11/54 +f 5/50 29/49 28/55 6/56 +f 31/57 35/58 37/59 33/60 +f 32/61 36/62 35/63 31/64 +f 32/65 34/66 38/67 36/68 +f 41/69 45/70 46/71 42/72 +f 38/67 42/73 40/74 36/68 +f 36/62 40/75 39/76 35/63 +f 35/58 39/77 41/78 37/59 +f 39/76 40/75 44/79 43/80 +f 39/77 43/81 45/82 41/78 +f 42/73 46/83 44/84 40/74 +f 44/84 46/83 50/85 48/86 +f 47/87 48/88 52/89 51/90 +f 46/71 45/70 49/91 50/92 +f 43/80 44/79 48/88 47/87 +f 45/82 43/81 47/93 49/94 +f 59/95 60/96 57/97 58/98 +f 49/94 47/93 51/99 53/100 +f 48/86 50/85 54/101 52/102 +f 50/92 49/91 53/103 54/104 +f 57/105 55/106 56/107 58/108 +f 61/109 62/110 56/111 55/112 +f 60/113 61/114 55/115 57/116 +f 62/117 59/118 58/119 56/120 +f 54/104 53/103 60/96 59/95 +f 51/90 52/89 62/110 61/109 +f 53/100 51/99 61/114 60/113 +f 52/102 54/101 59/118 62/117 +f 38/121 37/122 41/69 42/72 +f 124/123 130/124 131/125 125/126 +f 127/127 133/128 134/129 128/130 +f 129/131 135/132 136/133 130/134 +f 130/124 136/135 137/136 131/125 +f 131/137 137/138 138/139 132/140 +f 132/140 138/139 139/141 133/142 +f 133/128 139/143 140/144 134/129 +f 134/145 140/146 135/132 129/131 +f 135/147 141/148 142/149 136/150 +f 136/135 142/151 143/152 137/136 +f 137/153 143/154 144/155 138/156 +f 138/156 144/155 145/157 139/158 +f 139/143 145/159 146/160 140/144 +f 140/161 146/162 141/148 135/147 +f 141/163 123/164 124/165 142/166 +f 142/151 124/123 125/126 143/152 +f 143/167 125/168 126/169 144/170 +f 144/170 126/169 127/171 145/172 +f 145/159 127/127 128/130 146/160 +f 146/173 128/174 123/164 141/163 +f 181/175 185/176 186/177 182/178 +f 178/179 154/180 155/181 175/182 +f 177/183 157/184 158/185 176/186 +f 153/187 159/188 160/189 154/190 +f 154/191 160/192 161/193 155/194 +f 155/195 161/196 162/197 156/198 +f 156/198 162/197 163/199 157/200 +f 157/201 163/202 164/203 158/204 +f 158/204 164/203 159/188 153/187 +f 171/205 166/206 167/207 174/208 +f 173/209 169/210 170/211 172/212 +f 166/213 148/214 149/215 167/216 +f 167/216 149/215 150/217 168/218 +f 168/219 150/220 151/221 169/222 +f 169/223 151/224 152/225 170/226 +f 170/227 152/228 147/229 165/230 +f 164/231 172/232 170/233 165/234 166/235 171/236 160/237 159/238 +f 160/237 171/236 174/239 161/240 +f 173/209 163/241 162/242 161/243 174/208 167/207 168/244 169/210 +f 163/245 173/246 172/232 164/231 +f 148/247 178/248 175/249 149/250 +f 151/251 177/252 176/253 152/254 +f 179/255 183/256 185/257 181/258 +f 180/259 184/260 183/261 179/262 +f 180/263 182/264 186/265 184/266 +f 186/265 190/267 188/268 184/266 +f 184/260 188/269 187/270 183/261 +f 183/256 187/271 189/272 185/257 +f 187/270 188/269 192/273 191/274 +f 186/177 185/176 189/275 190/276 +f 187/271 191/277 193/278 189/272 +f 190/267 194/279 192/280 188/268 +f 192/280 194/279 198/281 196/282 +f 195/283 196/284 200/285 199/286 +f 194/287 193/288 197/289 198/290 +f 191/274 192/273 196/284 195/283 +f 193/278 191/277 195/291 197/292 +f 207/293 208/294 205/295 206/296 +f 197/292 195/291 199/297 201/298 +f 196/282 198/281 202/299 200/300 +f 198/290 197/289 201/301 202/302 +f 205/303 203/304 204/305 206/306 +f 209/307 210/308 204/309 203/310 +f 208/311 209/312 203/313 205/314 +f 210/315 207/316 206/317 204/318 +f 202/302 201/301 208/294 207/293 +f 199/286 200/285 210/308 209/307 +f 201/298 199/297 209/312 208/311 +f 200/300 202/299 207/316 210/315 +f 189/275 193/288 194/287 190/276 +f 63/319 69/320 70/321 64/322 +f 64/322 70/321 71/323 65/324 +f 96/325 90/326 89/327 95/328 +f 66/329 72/330 73/331 67/332 +f 67/333 73/334 74/335 68/336 +f 69/320 75/337 76/338 70/321 +f 70/321 76/338 77/339 71/323 +f 72/330 66/329 65/340 71/341 +f 72/330 78/342 79/343 73/331 +f 73/344 79/345 80/346 74/347 +f 75/348 81/349 82/350 76/351 +f 76/351 82/350 83/352 77/353 +f 119/354 120/355 114/356 113/357 +f 78/342 84/358 85/359 79/343 +f 79/345 85/360 86/361 80/346 +f 81/349 87/362 88/363 82/350 +f 82/350 88/363 89/364 83/352 +f 84/358 90/326 91/365 85/359 +f 85/360 91/366 92/367 86/361 +f 87/368 93/369 94/370 88/371 +f 88/372 94/373 95/328 89/327 +f 113/357 114/356 108/374 107/375 +f 90/326 96/325 97/376 91/365 +f 91/377 97/378 98/379 92/380 +f 93/369 99/381 100/382 94/370 +f 94/373 100/383 101/384 95/328 +f 83/385 84/358 78/342 77/386 +f 96/325 102/387 103/388 97/376 +f 97/378 103/389 104/390 98/379 +f 99/391 105/392 106/393 100/394 +f 100/383 106/395 107/375 101/384 +f 90/326 84/358 83/385 89/327 +f 102/387 108/374 109/396 103/388 +f 103/389 109/397 110/398 104/390 +f 105/392 111/399 112/400 106/393 +f 106/393 112/400 113/401 107/402 +f 102/387 96/325 95/328 101/384 +f 108/374 114/356 115/403 109/396 +f 109/404 115/405 116/406 110/407 +f 111/399 117/408 118/409 112/400 +f 112/400 118/409 119/410 113/401 +f 66/329 120/355 119/354 65/340 +f 114/356 120/355 121/411 115/403 +f 115/405 121/412 122/413 116/406 +f 78/342 72/330 71/341 77/386 +f 117/414 63/319 64/322 118/415 +f 118/416 64/417 65/340 119/354 +f 108/374 102/387 101/384 107/375 +f 120/355 66/329 67/332 121/411 +f 121/418 67/333 68/336 122/419 +f 80/420 75/421 69/422 74/423 +f 81/424 75/421 80/420 86/425 +f 87/426 81/424 86/425 92/427 +f 98/428 93/429 87/426 92/427 +f 104/430 99/431 93/429 98/428 +f 110/432 105/433 99/431 104/430 +f 111/434 105/433 110/432 116/435 +f 122/436 117/437 111/434 116/435 +f 122/436 68/438 63/439 117/437 +f 74/423 69/422 63/439 68/438 diff --git a/christmastree/models/indoor-christmas-tree.obj b/christmastree/models/indoor-christmas-tree.obj new file mode 100644 index 0000000..1033820 --- /dev/null +++ b/christmastree/models/indoor-christmas-tree.obj @@ -0,0 +1,875 @@ +# Blender v2.71 (sub 0) OBJ File: 'indoor-christmas-tree.blend' +# www.blender.org +o Cube.001_Cube.002 +v 0.063161 -0.481361 -0.357331 +v 0.062869 -0.360156 -0.091198 +v -0.229477 -0.353768 -0.094429 +v -0.229184 -0.474972 -0.360562 +v 0.065311 -0.401400 -0.393744 +v 0.065018 -0.280196 -0.127612 +v -0.227327 -0.273808 -0.130843 +v -0.227034 -0.395012 -0.396976 +v 0.364089 -0.494747 0.260180 +v 0.364089 -0.494747 0.552614 +v 0.071656 -0.494747 0.552614 +v 0.071656 -0.494747 0.260180 +v 0.364089 -0.202314 0.260180 +v 0.364089 -0.202314 0.552614 +v 0.071656 -0.202314 0.552614 +v 0.071656 -0.202314 0.260180 +v 0.423160 -0.385025 -0.035821 +v 0.419655 -0.382416 0.110330 +v 0.332759 -0.500011 0.110344 +v 0.336265 -0.502620 -0.035807 +v 0.188075 -0.211313 -0.044561 +v 0.184569 -0.208704 0.101591 +v 0.097674 -0.326299 0.101605 +v 0.101180 -0.328907 -0.044546 +v -0.308783 -0.451097 -0.373639 +v -0.343665 -0.497940 -0.239593 +v -0.469522 -0.423831 -0.246446 +v -0.434641 -0.376987 -0.380492 +v -0.177292 -0.217064 -0.257639 +v -0.212174 -0.263907 -0.123593 +v -0.338031 -0.189798 -0.130445 +v -0.303150 -0.142955 -0.264491 +v -0.256514 -0.494747 -0.143510 +v -0.256514 -0.494747 0.148923 +v -0.548947 -0.494747 0.148923 +v -0.548947 -0.494747 -0.143510 +v -0.256514 -0.278543 -0.143510 +v -0.256514 -0.278543 0.148923 +v -0.548947 -0.278543 0.148923 +v -0.548947 -0.278543 -0.143510 +v 0.016497 -0.176949 0.822118 +v 0.016497 2.225140 0.005725 +v -0.518721 -0.072056 0.537924 +v 0.014362 2.225140 0.004841 +v -0.802915 -0.176949 0.002707 +v 0.013478 2.225140 0.002707 +v -0.518721 -0.072056 -0.532510 +v 0.014362 2.225140 0.000573 +v 0.016497 -0.176949 -0.816705 +v 0.016497 2.225140 -0.000311 +v 0.551714 -0.072056 -0.532510 +v 0.018631 2.225140 0.000573 +v 0.835908 -0.176949 0.002707 +v 0.019515 2.225140 0.002707 +v 0.551714 -0.072056 0.537924 +v 0.018631 2.225140 0.004841 +v 0.016497 0.340839 0.614356 +v 0.016497 0.803725 0.521561 +v 0.016497 1.279110 0.372294 +v 0.016497 1.606774 0.266892 +v -0.170311 1.606774 0.189514 +v -0.244842 1.279110 0.264045 +v -0.350389 0.803725 0.369592 +v -0.416005 0.340839 0.435208 +v -0.247689 1.606774 0.002707 +v -0.353091 1.279110 0.002707 +v -0.502358 0.803725 0.002707 +v -0.595153 0.340839 0.002707 +v -0.170311 1.606774 -0.184100 +v -0.244842 1.279110 -0.258631 +v -0.350389 0.803725 -0.364179 +v -0.416005 0.340839 -0.429795 +v 0.016497 1.606774 -0.261479 +v 0.016497 1.279110 -0.366881 +v 0.016497 0.803725 -0.516148 +v 0.016497 0.340839 -0.608943 +v 0.203304 1.606774 -0.184100 +v 0.277835 1.279110 -0.258631 +v 0.383382 0.803725 -0.364179 +v 0.448998 0.340839 -0.429795 +v 0.280682 1.606774 0.002707 +v 0.386084 1.279110 0.002707 +v 0.535351 0.803725 0.002707 +v 0.628146 0.340839 0.002707 +v 0.203304 1.606774 0.189514 +v 0.277835 1.279110 0.264045 +v 0.383382 0.803725 0.369592 +v 0.448998 0.340839 0.435208 +v -0.029966 0.305894 0.679303 +v -0.537002 0.232208 0.485071 +v -0.660100 0.305893 -0.043756 +v -0.465868 0.232208 -0.550792 +v 0.062959 0.305893 -0.673890 +v 0.569995 0.232208 -0.479657 +v 0.693093 0.305893 0.049169 +v 0.498861 0.232208 0.556205 +v 0.058078 0.727034 0.629602 +v -0.365814 0.762976 0.439337 +v -0.610399 0.727034 0.044288 +v -0.420134 0.762976 -0.379604 +v -0.025085 0.727034 -0.624188 +v 0.398807 0.762976 -0.433924 +v 0.643392 0.727034 -0.038875 +v 0.453127 0.762976 0.385017 +v -0.017861 1.216826 0.462077 +v -0.361450 1.218359 0.328052 +v -0.442874 1.216826 -0.031651 +v -0.308849 1.218359 -0.375240 +v 0.050854 1.216826 -0.456664 +v 0.394443 1.218359 -0.322639 +v 0.475867 1.216826 0.037064 +v 0.341842 1.218359 0.380653 +v 0.048839 1.582472 0.335358 +v -0.179657 1.554458 0.241111 +v -0.316155 1.582472 0.035049 +v -0.221907 1.554458 -0.193447 +v -0.015846 1.582472 -0.329945 +v 0.212650 1.554458 -0.235697 +v 0.349148 1.582472 -0.029636 +v 0.254901 1.554458 0.198860 +v 0.016497 1.873582 0.138011 +v -0.079178 1.873582 0.098382 +v -0.118808 1.873582 0.002707 +v -0.079178 1.873582 -0.092968 +v 0.016497 1.873582 -0.132598 +v 0.112171 1.873582 -0.092968 +v 0.151801 1.873582 0.002707 +v 0.112171 1.873582 0.098382 +v -0.002514 1.851223 0.190368 +v -0.141710 1.905141 0.131808 +v -0.171165 1.851223 -0.016304 +v -0.112605 1.905141 -0.155500 +v 0.035507 1.851223 -0.184955 +v 0.174703 1.905141 -0.126395 +v 0.204158 1.851223 0.021717 +v 0.145598 1.905141 0.160913 +v 0.016497 0.030028 0.069771 +v -0.030925 0.030028 0.050128 +v -0.050567 0.030028 0.002707 +v -0.030925 0.030028 -0.044715 +v 0.016497 0.030028 -0.064357 +v 0.063918 0.030028 -0.044715 +v 0.083561 0.030028 0.002707 +v 0.063918 0.030028 0.050128 +v -0.253401 -0.489433 0.212336 +v -0.322580 -0.489433 -0.039910 +v -0.193133 -0.489433 -0.267191 +v 0.059113 -0.489433 -0.336370 +v 0.286394 -0.489433 -0.206923 +v 0.355574 -0.489433 0.045323 +v 0.226126 -0.489433 0.272604 +v -0.026120 -0.489433 0.341784 +v -0.031751 -0.332371 -0.045541 +v 0.016497 -0.332371 -0.065526 +v 0.064745 -0.332371 0.050955 +v 0.016497 -0.332371 0.070940 +v -0.051736 -0.332371 0.002707 +v 0.084730 -0.332371 0.002707 +v -0.031751 -0.332371 0.050955 +v 0.064745 -0.332371 -0.045541 +v 0.016497 2.498106 0.003866 +v 0.015677 2.498106 0.003527 +v 0.015337 2.498106 0.002707 +v 0.015677 2.498106 0.001887 +v 0.016497 2.498106 0.001547 +v 0.017317 2.498106 0.001887 +v 0.017656 2.498106 0.002707 +v 0.017317 2.498106 0.003527 +v 0.016497 2.367410 -0.021747 +v 0.016497 2.344189 -0.111707 +v 0.016497 2.320968 -0.021747 +v -0.000795 2.320968 -0.014585 +v -0.000795 2.344189 -0.014585 +v -0.000795 2.367410 -0.014585 +v -0.000795 2.367410 0.019998 +v -0.000795 2.344189 0.019998 +v -0.000795 2.320968 0.019998 +v 0.016497 2.320968 0.027160 +v 0.016497 2.344189 0.117120 +v 0.016497 2.367410 0.027160 +v 0.033788 2.367410 0.019998 +v 0.033788 2.344189 0.019998 +v 0.033788 2.320968 0.019998 +v 0.040950 2.320968 0.002707 +v 0.130910 2.344189 0.002707 +v 0.040950 2.367410 0.002707 +v -0.007957 2.367410 0.002707 +v -0.097917 2.344189 0.002707 +v -0.007957 2.320968 0.002707 +v 0.033788 2.367410 -0.014585 +v 0.033788 2.344189 -0.014585 +v 0.033788 2.320968 -0.014585 +v 0.110646 -0.438986 -0.174004 +v -0.077653 -0.438986 0.179417 +v -0.041883 -0.438986 -0.188821 +v 0.074876 -0.438986 0.194234 +v -0.160214 -0.438986 -0.091443 +v 0.193207 -0.438986 0.096856 +v -0.175031 -0.438986 0.061086 +v 0.208024 -0.438986 -0.055673 +vt 0.305531 0.759306 +vt 0.305531 0.693376 +vt 0.322138 0.693115 +vt 0.322138 0.759045 +vt 0.127009 0.760524 +vt 0.127009 0.824752 +vt 0.107338 0.825393 +vt 0.107338 0.761165 +vt 0.432716 0.837587 +vt 0.432716 0.772633 +vt 0.451511 0.773747 +vt 0.451511 0.838701 +vt 0.125765 0.827112 +vt 0.125765 0.891270 +vt 0.107338 0.889552 +vt 0.155872 0.819549 +vt 0.218576 0.820881 +vt 0.218576 0.886696 +vt 0.155872 0.885364 +vt 0.269209 0.757985 +vt 0.269209 0.692055 +vt 0.144607 0.399709 +vt 0.082230 0.399709 +vt 0.076435 0.342636 +vt 0.152993 0.348495 +vt 0.065629 0.383103 +vt 0.002115 0.387857 +vt 0.002622 0.335255 +vt 0.066818 0.331689 +vt 0.075973 0.468467 +vt 0.003549 0.468467 +vt 0.004133 0.406109 +vt 0.077011 0.399096 +vt 0.183275 0.409560 +vt 0.177073 0.360698 +vt 0.236889 0.355149 +vt 0.248670 0.406724 +vt 0.180198 0.425232 +vt 0.234253 0.418784 +vt 0.234253 0.484486 +vt 0.176292 0.481134 +vt 0.156500 0.465367 +vt 0.091662 0.465367 +vt 0.092722 0.421227 +vt 0.155100 0.421227 +vt 0.988916 0.422851 +vt 0.956702 0.419996 +vt 0.956702 0.359115 +vt 0.988916 0.361971 +vt 0.956702 0.292695 +vt 0.987695 0.293240 +vt 0.987695 0.359115 +vt 0.956702 0.358569 +vt 0.318028 0.833008 +vt 0.285092 0.832755 +vt 0.282500 0.776573 +vt 0.315436 0.776827 +vt 0.983284 0.658918 +vt 0.952201 0.659873 +vt 0.952201 0.598107 +vt 0.983284 0.597151 +vt 0.286551 0.760148 +vt 0.316320 0.760378 +vt 0.313977 0.775985 +vt 0.284208 0.775755 +vt 0.655221 0.272244 +vt 0.655221 0.244094 +vt 0.682642 0.243798 +vt 0.682642 0.271948 +vt 0.302769 0.660713 +vt 0.276136 0.668765 +vt 0.293661 0.624807 +vt 0.320294 0.616756 +vt 0.652886 0.289324 +vt 0.620133 0.292421 +vt 0.620133 0.244474 +vt 0.652886 0.241376 +vt 0.282500 0.833008 +vt 0.315357 0.833386 +vt 0.315357 0.898986 +vt 0.282500 0.898608 +vt 0.957349 0.757730 +vt 0.989329 0.762049 +vt 0.989329 0.818542 +vt 0.957349 0.814222 +vt 0.684976 0.277951 +vt 0.684976 0.248220 +vt 0.713984 0.241376 +vt 0.713984 0.271108 +vt 0.320294 0.683939 +vt 0.293661 0.691990 +vt 0.902832 0.512454 +vt 0.902832 0.447791 +vt 0.945142 0.452591 +vt 0.945142 0.517254 +vt 0.315192 0.908185 +vt 0.315192 0.972481 +vt 0.268191 0.969494 +vt 0.268191 0.905199 +vt 0.996073 0.532109 +vt 0.996073 0.593141 +vt 0.954279 0.592889 +vt 0.954279 0.531857 +vt 0.993996 0.447791 +vt 0.993996 0.512101 +vt 0.945142 0.512514 +vt 0.945142 0.448203 +vt 0.202418 0.970901 +vt 0.136645 0.971490 +vt 0.136645 0.905788 +vt 0.202418 0.905199 +vt 0.869699 0.523668 +vt 0.869699 0.459004 +vt 0.955471 0.888456 +vt 0.984249 0.976566 +vt 0.983853 0.976734 +vt 0.930084 0.910567 +vt 0.981099 0.186690 +vt 0.972680 0.271483 +vt 0.972193 0.271547 +vt 0.945451 0.181484 +vt 0.971754 0.271755 +vt 0.918474 0.205118 +vt 0.638049 0.971627 +vt 0.685335 0.913689 +vt 0.685819 0.913864 +vt 0.665808 0.994702 +vt 0.686215 0.914021 +vt 0.697596 0.994127 +vt 0.638049 0.896656 +vt 0.631957 0.981660 +vt 0.631444 0.981728 +vt 0.602256 0.892395 +vt 0.734294 0.507019 +vt 0.730123 0.507081 +vt 0.732680 0.477770 +vt 0.732878 0.477767 +vt 0.991373 0.893387 +vt 0.984758 0.976480 +vt 0.631061 0.981964 +vt 0.577426 0.917204 +vt 0.000000 0.655112 +vt 0.125495 0.652240 +vt 0.136645 0.738689 +vt 0.040491 0.760524 +vt 0.705027 0.634844 +vt 0.586885 0.635654 +vt 0.577426 0.562322 +vt 0.711147 0.529392 +vt 0.711147 0.857971 +vt 0.847265 0.853116 +vt 0.854003 0.905353 +vt 0.732523 0.938180 +vt 0.460232 0.861128 +vt 0.557232 0.857035 +vt 0.562048 0.894374 +vt 0.467289 0.933467 +vt 0.667359 0.003417 +vt 0.759805 0.026125 +vt 0.744719 0.121418 +vt 0.622139 0.121397 +vt 0.266260 0.754098 +vt 0.139594 0.721044 +vt 0.152178 0.641554 +vt 0.264724 0.641691 +vt 0.748983 0.292695 +vt 0.869699 0.326498 +vt 0.860318 0.385756 +vt 0.722592 0.385846 +vt 0.339097 0.776111 +vt 0.430415 0.811853 +vt 0.424003 0.852359 +vt 0.329838 0.852460 +vt 0.759805 0.215656 +vt 0.667359 0.237960 +vt 0.139594 0.562945 +vt 0.266260 0.530384 +vt 0.869699 0.444358 +vt 0.748983 0.477767 +vt 0.430415 0.892416 +vt 0.339097 0.928186 +vt 0.479105 0.543041 +vt 0.571801 0.558061 +vt 0.575216 0.662073 +vt 0.460232 0.675489 +vt 0.840286 0.757730 +vt 0.712460 0.732936 +vt 0.711147 0.644666 +vt 0.822262 0.635762 +vt 0.593343 0.326602 +vt 0.566633 0.441420 +vt 0.503750 0.441070 +vt 0.492299 0.318297 +vt 0.953920 0.442831 +vt 0.873107 0.417140 +vt 0.872482 0.375070 +vt 0.953371 0.365192 +vt 0.575216 0.748257 +vt 0.483331 0.778407 +vt 0.712460 0.571570 +vt 0.840286 0.529392 +vt 0.451713 0.439072 +vt 0.412888 0.322916 +vt 0.873107 0.340232 +vt 0.953920 0.297655 +vt 0.630153 0.292695 +vt 0.722592 0.320635 +vt 0.702295 0.423140 +vt 0.577426 0.419612 +vt 0.331137 0.543041 +vt 0.460232 0.583559 +vt 0.443014 0.670513 +vt 0.327538 0.665571 +vt 0.793532 0.000000 +vt 0.911423 0.038698 +vt 0.899158 0.100636 +vt 0.761810 0.098299 +vt 0.015270 0.765702 +vt 0.103774 0.802048 +vt 0.095589 0.843383 +vt 0.003564 0.842334 +vt 0.040491 0.525175 +vt 0.136645 0.548506 +vt 0.711147 0.757677 +vt 0.577426 0.723651 +vt 0.732523 0.757730 +vt 0.854003 0.790435 +vt 0.467289 0.778407 +vt 0.562048 0.812230 +vt 0.722592 0.511907 +vt 0.630153 0.529392 +vt 0.460232 0.745812 +vt 0.331137 0.772633 +vt 0.911423 0.154273 +vt 0.793532 0.181484 +vt 0.103774 0.879177 +vt 0.015270 0.911295 +vt 0.340901 0.185305 +vt 0.327538 0.190013 +vt 0.331296 0.068638 +vt 0.352374 0.080440 +vt 0.431125 0.000000 +vt 0.434325 0.013935 +vt 0.555010 0.001787 +vt 0.538749 0.024747 +vt 0.619786 0.102682 +vt 0.604475 0.106542 +vt 0.620133 0.225844 +vt 0.593003 0.211407 +vt 0.516199 0.292695 +vt 0.511052 0.277912 +vt 0.406628 0.267100 +vt 0.396419 0.292695 +vt 0.499634 0.248092 +vt 0.506198 0.268139 +vt 0.411962 0.245819 +vt 0.417980 0.235934 +vt 0.347441 0.173379 +vt 0.368527 0.169836 +vt 0.369753 0.080930 +vt 0.380243 0.088518 +vt 0.441576 0.015696 +vt 0.446267 0.039615 +vt 0.534934 0.039831 +vt 0.527921 0.051773 +vt 0.600332 0.110456 +vt 0.577375 0.117871 +vt 0.565658 0.199189 +vt 0.577143 0.204719 +vt 0.539785 0.185019 +vt 0.555259 0.193453 +vt 0.496740 0.243142 +vt 0.491452 0.218687 +vt 0.422646 0.222867 +vt 0.433388 0.208618 +vt 0.372810 0.164914 +vt 0.399606 0.160711 +vt 0.393364 0.091261 +vt 0.409895 0.103029 +vt 0.451910 0.041654 +vt 0.458228 0.069362 +vt 0.525977 0.061847 +vt 0.516292 0.079430 +vt 0.550074 0.127337 +vt 0.575840 0.119882 +vt 0.466033 0.091108 +vt 0.462379 0.071313 +vt 0.513289 0.088820 +vt 0.505370 0.099957 +vt 0.546971 0.130636 +vt 0.527089 0.133925 +vt 0.530050 0.182447 +vt 0.518467 0.173114 +vt 0.488040 0.214655 +vt 0.484555 0.194568 +vt 0.436305 0.198578 +vt 0.445218 0.185719 +vt 0.403448 0.155333 +vt 0.423499 0.151751 +vt 0.432121 0.112562 +vt 0.419544 0.104951 +vt 0.842279 0.240597 +vt 0.916008 0.233238 +vt 0.918474 0.252361 +vt 0.843510 0.286613 +vt 0.766404 0.181484 +vt 0.842279 0.213768 +vt 0.838844 0.235462 +vt 0.761810 0.235818 +vt 0.842279 0.256916 +vt 0.766404 0.289305 +vt 0.388616 0.995064 +vt 0.421347 0.931664 +vt 0.444263 0.934429 +vt 0.448174 0.995064 +vt 0.972433 0.145527 +vt 0.911423 0.096571 +vt 0.920388 0.082733 +vt 1.000000 0.104665 +vt 0.921613 0.059652 +vt 1.000000 0.045656 +vt 0.843510 0.181484 +vt 0.918474 0.210290 +vt 0.914380 0.040848 +vt 0.984986 0.000000 +vt 0.493760 0.115204 +vt 0.500196 0.104527 +vt 0.525233 0.132685 +vt 0.507839 0.136304 +vt 0.513152 0.168686 +vt 0.503054 0.161227 +vt 0.486550 0.191650 +vt 0.482208 0.175375 +vt 0.449663 0.181555 +vt 0.457512 0.170459 +vt 0.428032 0.152388 +vt 0.443433 0.149360 +vt 0.436707 0.117397 +vt 0.448218 0.124436 +vt 0.469064 0.110289 +vt 0.466715 0.093423 +vt 0.112768 0.326771 +vt 0.003272 0.251449 +vt 0.145979 0.169903 +vt 0.154861 0.177225 +vt 0.079082 0.001308 +vt 0.211802 0.000000 +vt 0.163636 0.148273 +vt 0.152183 0.149431 +vt 0.324570 0.212584 +vt 0.251728 0.326771 +vt 0.166313 0.176068 +vt 0.173627 0.167108 +vt 0.327538 0.076629 +vt 0.172518 0.155595 +vt 0.000000 0.114187 +vt 0.144869 0.158390 +vt 0.612887 0.816449 +vt 0.615530 0.835772 +vt 0.603716 0.863090 +vt 0.577426 0.826127 +vt 0.926551 0.777826 +vt 0.893641 0.778745 +vt 0.854003 0.765765 +vt 0.897846 0.757730 +vt 0.890392 0.566292 +vt 0.872462 0.582147 +vt 0.840286 0.589631 +vt 0.848461 0.564553 +vt 0.957349 0.783870 +vt 0.956802 0.762024 +vt 0.923104 0.555466 +vt 0.894292 0.539601 +vt 0.639881 0.856393 +vt 0.671677 0.866234 +vt 0.703716 0.892395 +vt 0.656027 0.890539 +vt 0.951435 0.556009 +vt 0.950931 0.529392 +vt 0.299671 0.507235 +vt 0.310980 0.508964 +vt 0.298114 0.589611 +vt 0.286607 0.587851 +vt 0.924610 0.888456 +vt 0.913206 0.887237 +vt 0.918168 0.808593 +vt 0.929772 0.809834 +vt 0.940607 0.663336 +vt 0.930845 0.667938 +vt 0.909935 0.595313 +vt 0.919867 0.590632 +vt 0.693787 0.763629 +vt 0.703716 0.764895 +vt 0.671902 0.828695 +vt 0.661801 0.827407 +vt 0.951435 0.661962 +vt 0.930884 0.589234 +vt 0.683972 0.757677 +vt 0.651814 0.821351 +vt 0.903702 0.888456 +vt 0.908499 0.809834 +vt 0.320294 0.510701 +vt 0.307590 0.591378 +vt 0.722592 0.478952 +vt 0.725946 0.477974 +vt 0.729925 0.507498 +vt 0.729766 0.507544 +vt 0.732816 0.242991 +vt 0.736123 0.241451 +vt 0.740113 0.269380 +vt 0.739956 0.269453 +vt 0.279899 0.574978 +vt 0.283878 0.575073 +vt 0.283878 0.603975 +vt 0.283689 0.603970 +vt 0.737749 0.507081 +vt 0.733042 0.477770 +vt 0.730123 0.477767 +vt 0.730123 0.507488 +vt 0.276127 0.576106 +vt 0.283510 0.604024 +vt 0.740311 0.241376 +vt 0.740311 0.269376 +vt 0.690465 0.892395 +vt 0.694388 0.893811 +vt 0.320294 0.414965 +vt 0.323851 0.412734 +vt 0.324205 0.412126 +vt 0.324205 0.434221 +vt 0.326518 0.348775 +vt 0.326518 0.370674 +vt 0.321682 0.370314 +vt 0.320294 0.368654 +vt 0.987249 0.995924 +vt 0.984034 0.997287 +vt 0.719463 0.259661 +vt 0.717858 0.255759 +vt 0.732816 0.241376 +vt 0.720167 0.259600 +vt 0.326516 0.326771 +vt 0.320294 0.328835 +vt 0.321675 0.327070 +vt 0.633897 0.998092 +vt 0.630789 1.000000 +vt 0.286411 0.529699 +vt 0.286024 0.532308 +vt 0.286411 0.552403 +vt 0.281463 0.530744 +vt 0.276127 0.534399 +vt 0.276514 0.531790 +vt 0.981099 0.288451 +vt 0.977153 0.288967 +vt 0.717858 0.263558 +vt 0.732816 0.277864 +vt 0.326354 0.391578 +vt 0.326354 0.412126 +vt 0.321330 0.411542 +vt 0.320294 0.410747 +vt 0.697596 0.895090 +vt 0.285130 0.555116 +vt 0.286359 0.574978 +vt 0.281858 0.553211 +vt 0.276127 0.556733 +vt 0.277357 0.554020 +vt 0.991373 0.995230 +vt 0.713984 0.254138 +vt 0.714687 0.254077 +vt 0.326634 0.370674 +vt 0.326634 0.391578 +vt 0.322158 0.390641 +vt 0.320294 0.389542 +vt 0.973600 0.290658 +vt 0.286607 0.507784 +vt 0.286607 0.510227 +vt 0.280158 0.529699 +vt 0.281367 0.507509 +vt 0.276127 0.509677 +vt 0.276127 0.507235 +vt 0.638049 0.997539 +vt 0.722630 0.511027 +vt 0.722592 0.510301 +vt 0.727654 0.508923 +vt 0.732754 0.528777 +vt 0.732716 0.507544 +vt 0.732754 0.508271 +vt 0.287880 0.611178 +vt 0.936512 0.812829 +vt 0.936532 0.591938 +vt 0.647794 0.814075 +s off +f 5/1 6/2 2/3 1/4 +f 6/5 7/6 3/7 2/8 +f 7/9 8/10 4/11 3/12 +f 8/13 5/14 1/15 4/7 +f 1/16 2/17 3/18 4/19 +f 8/20 7/21 6/2 5/1 +f 13/22 14/23 10/24 9/25 +f 14/26 15/27 11/28 10/29 +f 15/30 16/31 12/32 11/33 +f 16/34 13/35 9/36 12/37 +f 9/38 10/39 11/40 12/41 +f 16/42 15/43 14/44 13/45 +f 21/46 22/47 18/48 17/49 +f 22/50 23/51 19/52 18/53 +f 23/54 24/55 20/56 19/57 +f 24/58 21/59 17/60 20/61 +f 17/62 18/63 19/64 20/65 +f 24/66 23/67 22/68 21/69 +f 29/70 30/71 26/72 25/73 +f 30/74 31/75 27/76 26/77 +f 31/78 32/79 28/80 27/81 +f 32/82 29/83 25/84 28/85 +f 25/86 26/87 27/88 28/89 +f 32/90 31/91 30/71 29/70 +f 37/92 38/93 34/94 33/95 +f 38/96 39/97 35/98 34/99 +f 39/100 40/101 36/102 35/103 +f 40/104 37/105 33/106 36/107 +f 33/108 34/109 35/110 36/111 +f 40/112 39/113 38/93 37/92 +f 129/114 42/115 44/116 130/117 +f 130/118 44/119 46/120 131/121 +f 131/121 46/120 48/122 132/123 +f 132/124 48/125 50/126 133/127 +f 133/127 50/126 52/128 134/129 +f 134/130 52/131 54/132 135/133 +f 169/134 174/135 164/136 165/137 +f 136/138 56/139 42/115 129/114 +f 135/133 54/132 56/140 136/141 +f 41/142 57/143 64/144 43/145 +f 89/146 58/147 63/148 90/149 +f 97/150 59/151 62/152 98/153 +f 105/154 60/155 61/156 106/157 +f 43/158 64/159 68/160 45/161 +f 90/162 63/163 67/164 91/165 +f 98/166 62/167 66/168 99/169 +f 106/170 61/171 65/172 107/173 +f 45/161 68/160 72/174 47/175 +f 91/165 67/164 71/176 92/177 +f 99/169 66/168 70/178 100/179 +f 107/173 65/172 69/180 108/181 +f 47/182 72/183 76/184 49/185 +f 92/186 71/187 75/188 93/189 +f 100/190 70/191 74/192 101/193 +f 108/194 69/195 73/196 109/197 +f 49/185 76/184 80/198 51/199 +f 93/189 75/188 79/200 94/201 +f 101/193 74/192 78/202 102/203 +f 109/197 73/196 77/204 110/205 +f 51/206 80/207 84/208 53/209 +f 94/210 79/211 83/212 95/213 +f 102/214 78/215 82/216 103/217 +f 110/218 77/219 81/220 111/221 +f 55/222 88/223 57/143 41/142 +f 96/224 87/225 58/147 89/146 +f 104/226 86/227 59/151 97/150 +f 112/228 85/229 60/155 105/154 +f 53/209 84/208 88/230 55/231 +f 95/213 83/212 87/232 96/233 +f 103/217 82/216 86/234 104/235 +f 111/221 81/220 85/236 112/237 +f 57/238 89/239 90/240 64/241 +f 64/241 90/240 91/242 68/243 +f 68/243 91/242 92/244 72/245 +f 72/245 92/244 93/246 76/247 +f 76/247 93/246 94/248 80/249 +f 80/249 94/248 95/250 84/251 +f 88/252 96/253 89/239 57/238 +f 84/251 95/250 96/253 88/252 +f 58/254 97/255 98/256 63/257 +f 63/257 98/256 99/258 67/259 +f 67/259 99/258 100/260 71/261 +f 71/261 100/260 101/262 75/263 +f 75/263 101/262 102/264 79/265 +f 79/265 102/264 103/266 83/267 +f 87/268 104/269 97/255 58/254 +f 83/267 103/266 104/269 87/268 +f 59/270 105/271 106/272 62/273 +f 62/273 106/272 107/274 66/275 +f 66/275 107/274 108/276 70/277 +f 70/277 108/276 109/278 74/279 +f 74/279 109/278 110/280 78/281 +f 78/281 110/280 111/282 82/283 +f 86/284 112/285 105/271 59/270 +f 82/283 111/282 112/285 86/284 +f 60/286 113/287 114/288 61/289 +f 61/289 114/288 115/290 65/291 +f 65/291 115/290 116/292 69/293 +f 69/293 116/292 117/294 73/295 +f 73/295 117/294 118/296 77/297 +f 77/297 118/296 119/298 81/299 +f 85/300 120/301 113/287 60/286 +f 81/299 119/298 120/301 85/300 +f 113/302 121/303 122/304 114/305 +f 114/306 122/307 123/308 115/309 +f 115/309 123/308 124/310 116/311 +f 116/312 124/313 125/314 117/315 +f 117/316 125/317 126/318 118/319 +f 118/319 126/318 127/320 119/321 +f 120/322 128/323 121/303 113/302 +f 119/321 127/320 128/324 120/325 +f 121/326 129/327 130/328 122/329 +f 122/329 130/328 131/330 123/331 +f 123/331 131/330 132/332 124/333 +f 124/333 132/332 133/334 125/335 +f 125/335 133/334 134/336 126/337 +f 126/337 134/336 135/338 127/339 +f 128/340 136/341 129/327 121/326 +f 127/339 135/338 136/341 128/340 +f 55/342 41/343 137/344 144/345 +f 45/346 47/347 140/348 139/349 +f 51/350 53/351 143/352 142/353 +f 47/347 49/354 141/355 140/348 +f 41/343 43/356 138/357 137/344 +f 53/351 55/342 144/345 143/352 +f 43/356 45/346 139/349 138/357 +f 49/354 51/350 142/353 141/355 +f 195/358 193/359 149/360 148/361 +f 196/362 194/363 145/364 152/365 +f 197/366 195/367 148/368 147/369 +f 198/370 196/362 152/365 151/371 +f 199/372 197/366 147/369 146/373 +f 200/374 198/375 151/376 150/377 +f 194/378 199/372 146/373 145/379 +f 193/359 200/374 150/377 149/360 +f 140/380 141/381 154/382 153/383 +f 144/384 137/385 156/386 155/387 +f 139/388 140/389 153/390 157/391 +f 143/392 144/393 155/394 158/395 +f 138/396 139/388 157/391 159/397 +f 142/398 143/392 158/395 160/399 +f 137/385 138/400 159/401 156/386 +f 141/381 142/402 160/403 154/382 +f 175/404 180/405 161/406 162/407 +f 181/408 186/409 167/410 168/411 +f 187/412 175/413 162/414 163/415 +f 190/416 169/134 165/137 166/417 +f 180/405 181/418 168/419 161/406 +f 174/420 187/412 163/415 164/421 +f 186/409 190/422 166/423 167/410 +f 50/126 48/125 172/424 171/425 +f 171/426 172/427 173/428 170/429 +f 170/430 173/431 174/432 169/433 +f 44/116 42/115 178/434 177/435 +f 177/436 178/437 179/438 176/439 +f 176/440 179/430 180/441 175/442 +f 56/140 54/132 184/443 183/444 +f 183/445 184/446 185/447 182/448 +f 182/448 185/447 186/449 181/450 +f 46/120 44/119 177/451 189/452 +f 189/453 177/436 176/439 188/454 +f 188/455 176/456 175/457 187/458 +f 52/128 50/126 171/425 192/459 +f 192/447 171/460 170/461 191/462 +f 191/462 170/461 169/463 190/464 +f 42/115 56/139 183/465 178/434 +f 178/437 183/466 182/467 179/438 +f 179/468 182/469 181/470 180/471 +f 48/122 46/120 189/452 172/472 +f 172/473 189/474 188/475 173/476 +f 173/476 188/475 187/477 174/478 +f 54/132 52/131 192/479 184/443 +f 184/480 192/481 191/482 185/483 +f 185/483 191/482 190/484 186/485 +f 153/383 154/382 193/73 195/486 +f 155/387 156/386 194/363 196/362 +f 157/391 153/390 195/367 197/366 +f 158/487 155/387 196/362 198/370 +f 159/397 157/391 197/366 199/372 +f 160/399 158/395 198/375 200/374 +f 156/488 159/397 199/372 194/378 +f 154/489 160/399 200/374 193/359 diff --git a/christmastree/models/outdoor-christmas-tree.obj b/christmastree/models/outdoor-christmas-tree.obj new file mode 100644 index 0000000..2efedd3 --- /dev/null +++ b/christmastree/models/outdoor-christmas-tree.obj @@ -0,0 +1,633 @@ +# Blender v2.72 (sub 0) OBJ File: 'outdoor-christmas-tree.blend' +# www.blender.org +v 0.029152 -0.228498 1.452818 +v 0.029152 4.014762 0.010117 +v -0.916664 -0.043204 0.950599 +v 0.025381 4.014762 0.008554 +v -1.418883 -0.228498 0.004783 +v 0.023819 4.014762 0.004783 +v -0.916664 -0.043204 -0.941033 +v 0.025381 4.014762 0.001012 +v 0.029152 -0.228498 -1.443252 +v 0.029152 4.014762 -0.000550 +v 0.974969 -0.043204 -0.941033 +v 0.032924 4.014762 0.001012 +v 1.477187 -0.228498 0.004783 +v 0.034486 4.014762 0.004783 +v 0.974969 -0.043204 0.950599 +v 0.032924 4.014762 0.008554 +v 0.029152 0.686169 1.085668 +v 0.029152 1.503851 0.921684 +v 0.029152 2.343612 0.657905 +v 0.029152 2.922426 0.471642 +v -0.300967 2.922426 0.334902 +v -0.432675 2.343612 0.466610 +v -0.619195 1.503851 0.653130 +v -0.735149 0.686169 0.769084 +v -0.437706 2.922426 0.004783 +v -0.623970 2.343612 0.004783 +v -0.887748 1.503851 0.004783 +v -1.051733 0.686169 0.004783 +v -0.300967 2.922426 -0.325336 +v -0.432675 2.343612 -0.457044 +v -0.619195 1.503851 -0.643563 +v -0.735149 0.686169 -0.759518 +v 0.029152 2.922426 -0.462075 +v 0.029152 2.343612 -0.648339 +v 0.029152 1.503851 -0.912117 +v 0.029152 0.686169 -1.076102 +v 0.359271 2.922426 -0.325336 +v 0.490979 2.343612 -0.457044 +v 0.677499 1.503851 -0.643563 +v 0.793454 0.686169 -0.759518 +v 0.496011 2.922426 0.004783 +v 0.682274 2.343612 0.004783 +v 0.946053 1.503851 0.004783 +v 1.110037 0.686169 0.004783 +v 0.359271 2.922426 0.334902 +v 0.490979 2.343612 0.466610 +v 0.677499 1.503851 0.653130 +v 0.793454 0.686169 0.769084 +v -0.052955 0.624438 1.200440 +v -0.948970 0.494273 0.857200 +v -1.166505 0.624438 -0.077324 +v -0.823264 0.494273 -0.973339 +v 0.111259 0.624438 -1.190874 +v 1.007274 0.494273 -0.847633 +v 1.224809 0.624438 0.086890 +v 0.881569 0.494273 0.982905 +v 0.102633 1.368377 1.112609 +v -0.646453 1.431869 0.776380 +v -1.078674 1.368376 0.078264 +v -0.742445 1.431869 -0.670822 +v -0.044329 1.368376 -1.103043 +v 0.704758 1.431869 -0.766814 +v 1.136978 1.368376 -0.068698 +v 0.800749 1.431869 0.680388 +v -0.031563 2.233588 0.816566 +v -0.638741 2.236296 0.579722 +v -0.782631 2.233588 -0.055932 +v -0.545787 2.236296 -0.663110 +v 0.089867 2.233588 -0.807000 +v 0.697045 2.236296 -0.570156 +v 0.840936 2.233588 0.065498 +v 0.604091 2.236296 0.672676 +v 0.086306 2.879498 0.592633 +v -0.317484 2.830011 0.426082 +v -0.558698 2.879498 0.061937 +v -0.392147 2.830011 -0.341853 +v -0.028002 2.879498 -0.583067 +v 0.375788 2.830011 -0.416516 +v 0.617002 2.879498 -0.052371 +v 0.450451 2.830011 0.351419 +v 0.029152 3.393739 0.243889 +v -0.139921 3.393739 0.173856 +v -0.209953 3.393739 0.004783 +v -0.139921 3.393739 -0.164290 +v 0.029152 3.393739 -0.234322 +v 0.198225 3.393739 -0.164290 +v 0.268258 3.393739 0.004783 +v 0.198225 3.393739 0.173856 +v -0.004442 3.354242 0.336412 +v -0.250424 3.449487 0.232927 +v -0.302477 3.354242 -0.028811 +v -0.198991 3.449487 -0.274793 +v 0.062747 3.354242 -0.326845 +v 0.308729 3.449487 -0.223360 +v 0.360781 3.354242 0.038378 +v 0.257296 3.449487 0.284360 +v 0.029152 0.137126 0.123296 +v -0.054649 0.137126 0.088584 +v -0.089361 0.137126 0.004783 +v -0.054649 0.137126 -0.079018 +v 0.029152 0.137126 -0.113730 +v 0.112954 0.137126 -0.079018 +v 0.147665 0.137126 0.004783 +v 0.112954 0.137126 0.088584 +v -0.056110 -0.503049 -0.080479 +v 0.029152 -0.503049 -0.115796 +v 0.114414 -0.503049 0.090045 +v 0.029152 -0.503049 0.125362 +v -0.091427 -0.503049 0.004783 +v 0.149731 -0.503049 0.004783 +v -0.056110 -0.503049 0.090045 +v 0.114414 -0.503049 -0.080479 +v 0.029152 4.496952 0.006832 +v 0.027703 4.496952 0.006232 +v 0.027103 4.496952 0.004783 +v 0.027703 4.496952 0.003334 +v 0.029152 4.496952 0.002734 +v 0.030601 4.496952 0.003334 +v 0.031201 4.496952 0.004783 +v 0.030601 4.496952 0.006232 +v 0.029152 4.266080 -0.038430 +v 0.029152 4.225060 -0.197405 +v 0.029152 4.184040 -0.038430 +v -0.001404 4.184040 -0.025773 +v -0.001404 4.225060 -0.025773 +v -0.001404 4.266080 -0.025773 +v -0.001404 4.266080 0.035340 +v -0.001404 4.225060 0.035340 +v -0.001404 4.184040 0.035340 +v 0.029152 4.184040 0.047996 +v 0.029152 4.225060 0.206971 +v 0.029152 4.266080 0.047996 +v 0.059709 4.266080 0.035340 +v 0.059709 4.225060 0.035340 +v 0.059709 4.184040 0.035340 +v 0.072365 4.184040 0.004783 +v 0.231340 4.225060 0.004783 +v 0.072365 4.266080 0.004783 +v -0.014061 4.266080 0.004783 +v -0.173036 4.225060 0.004783 +v -0.014061 4.184040 0.004783 +v 0.059709 4.266080 -0.025773 +v 0.059709 4.225060 -0.025773 +v 0.059709 4.184040 -0.025773 +vt 0.955471 0.888456 +vt 0.984249 0.976566 +vt 0.983853 0.976734 +vt 0.930084 0.910567 +vt 0.981099 0.186690 +vt 0.972680 0.271483 +vt 0.972193 0.271547 +vt 0.945451 0.181484 +vt 0.971754 0.271755 +vt 0.918474 0.205118 +vt 0.638049 0.971627 +vt 0.685335 0.913689 +vt 0.685819 0.913864 +vt 0.665808 0.994702 +vt 0.686215 0.914021 +vt 0.697596 0.994127 +vt 0.638049 0.896656 +vt 0.631957 0.981660 +vt 0.631444 0.981728 +vt 0.602256 0.892395 +vt 0.734294 0.507019 +vt 0.730123 0.507081 +vt 0.732680 0.477770 +vt 0.732878 0.477767 +vt 0.991373 0.893387 +vt 0.984758 0.976480 +vt 0.631061 0.981964 +vt 0.577426 0.917204 +vt 0.000000 0.655112 +vt 0.125495 0.652240 +vt 0.136645 0.738689 +vt 0.040491 0.760524 +vt 0.705027 0.634844 +vt 0.586885 0.635654 +vt 0.577915 0.565251 +vt 0.711636 0.532322 +vt 0.711147 0.857971 +vt 0.847265 0.853116 +vt 0.854003 0.905353 +vt 0.732523 0.938180 +vt 0.460232 0.861128 +vt 0.557232 0.857035 +vt 0.562048 0.894374 +vt 0.467289 0.933467 +vt 0.667359 0.003417 +vt 0.759805 0.026125 +vt 0.744719 0.121418 +vt 0.622139 0.121397 +vt 0.266748 0.757027 +vt 0.140082 0.723974 +vt 0.152178 0.641554 +vt 0.264724 0.641691 +vt 0.748983 0.292695 +vt 0.869699 0.326498 +vt 0.860318 0.385756 +vt 0.722592 0.385846 +vt 0.339097 0.776111 +vt 0.430415 0.811853 +vt 0.424003 0.852359 +vt 0.329838 0.852460 +vt 0.755899 0.217609 +vt 0.663453 0.239913 +vt 0.144244 0.559845 +vt 0.270910 0.527283 +vt 0.869699 0.444358 +vt 0.748983 0.477767 +vt 0.430415 0.892416 +vt 0.339097 0.928186 +vt 0.475198 0.544994 +vt 0.567894 0.560014 +vt 0.575216 0.662073 +vt 0.460232 0.675489 +vt 0.844937 0.754629 +vt 0.717110 0.729836 +vt 0.711147 0.644666 +vt 0.822262 0.635762 +vt 0.593343 0.326602 +vt 0.566633 0.441420 +vt 0.503750 0.441070 +vt 0.492299 0.318297 +vt 0.953920 0.442831 +vt 0.873107 0.417140 +vt 0.872482 0.375070 +vt 0.953371 0.365192 +vt 0.575216 0.748257 +vt 0.483331 0.778407 +vt 0.712460 0.573034 +vt 0.840286 0.530857 +vt 0.454813 0.442560 +vt 0.415989 0.326404 +vt 0.873107 0.340232 +vt 0.953920 0.297655 +vt 0.630153 0.292695 +vt 0.722592 0.320635 +vt 0.702295 0.423140 +vt 0.577426 0.419612 +vt 0.331137 0.544506 +vt 0.460232 0.585024 +vt 0.443014 0.670513 +vt 0.327538 0.665571 +vt 0.796633 0.003488 +vt 0.914523 0.042186 +vt 0.899158 0.100636 +vt 0.761810 0.098299 +vt 0.015270 0.765702 +vt 0.103774 0.802048 +vt 0.095589 0.843383 +vt 0.003564 0.842334 +vt 0.040491 0.525175 +vt 0.136645 0.548506 +vt 0.711147 0.756901 +vt 0.577426 0.722876 +vt 0.732523 0.757730 +vt 0.854003 0.790435 +vt 0.467289 0.778407 +vt 0.562048 0.812230 +vt 0.722592 0.511907 +vt 0.630153 0.529392 +vt 0.460232 0.745037 +vt 0.331137 0.771858 +vt 0.911423 0.154273 +vt 0.793532 0.181484 +vt 0.103774 0.879177 +vt 0.015270 0.911295 +vt 0.340901 0.185305 +vt 0.327538 0.190013 +vt 0.331784 0.071568 +vt 0.352374 0.080440 +vt 0.431125 0.000000 +vt 0.434325 0.013935 +vt 0.559660 -0.001313 +vt 0.534842 0.026700 +vt 0.619786 0.102682 +vt 0.604475 0.106542 +vt 0.620133 0.227308 +vt 0.593003 0.211407 +vt 0.516199 0.292695 +vt 0.511052 0.277912 +vt 0.406628 0.267100 +vt 0.396419 0.291920 +vt 0.499634 0.248092 +vt 0.506198 0.268139 +vt 0.411962 0.245819 +vt 0.418468 0.238863 +vt 0.347441 0.173379 +vt 0.368527 0.169836 +vt 0.369753 0.080930 +vt 0.384894 0.085417 +vt 0.441576 0.015696 +vt 0.446267 0.039615 +vt 0.538034 0.043319 +vt 0.527921 0.053238 +vt 0.600332 0.110456 +vt 0.577375 0.117871 +vt 0.565658 0.198414 +vt 0.577143 0.204719 +vt 0.539785 0.185019 +vt 0.555259 0.193453 +vt 0.496740 0.243142 +vt 0.491452 0.218687 +vt 0.422646 0.222867 +vt 0.433388 0.208618 +vt 0.372810 0.164914 +vt 0.399606 0.160711 +vt 0.393364 0.091261 +vt 0.409895 0.103029 +vt 0.451910 0.041654 +vt 0.461328 0.072849 +vt 0.525977 0.061847 +vt 0.516292 0.079430 +vt 0.550074 0.127337 +vt 0.575840 0.119882 +vt 0.466033 0.091108 +vt 0.462379 0.071313 +vt 0.513289 0.088820 +vt 0.505370 0.099957 +vt 0.546971 0.130636 +vt 0.527089 0.133925 +vt 0.530050 0.182447 +vt 0.518467 0.173114 +vt 0.488040 0.214655 +vt 0.484555 0.194568 +vt 0.436305 0.198578 +vt 0.445218 0.185719 +vt 0.403448 0.155333 +vt 0.423499 0.151751 +vt 0.432121 0.112562 +vt 0.419544 0.104951 +vt 0.842279 0.240597 +vt 0.916008 0.233238 +vt 0.918474 0.252361 +vt 0.843510 0.286613 +vt 0.766404 0.181484 +vt 0.842279 0.213768 +vt 0.838844 0.235462 +vt 0.761810 0.235818 +vt 0.842279 0.256916 +vt 0.766404 0.289305 +vt 0.388616 0.995064 +vt 0.421347 0.931664 +vt 0.444263 0.934429 +vt 0.448174 0.995064 +vt 0.972433 0.145527 +vt 0.911423 0.096571 +vt 0.920388 0.082733 +vt 1.000000 0.104665 +vt 0.921613 0.059652 +vt 1.000000 0.045656 +vt 0.843510 0.181484 +vt 0.918474 0.210290 +vt 0.914380 0.040848 +vt 0.984986 0.000000 +vt 0.493760 0.115204 +vt 0.500196 0.104527 +vt 0.525233 0.132685 +vt 0.507839 0.136304 +vt 0.513152 0.168686 +vt 0.503054 0.161227 +vt 0.486550 0.191650 +vt 0.482208 0.175375 +vt 0.449663 0.181555 +vt 0.457512 0.170459 +vt 0.428032 0.152388 +vt 0.443433 0.149360 +vt 0.436707 0.117397 +vt 0.448218 0.124436 +vt 0.469064 0.110289 +vt 0.466715 0.093423 +vt 0.112768 0.326771 +vt 0.003272 0.251449 +vt 0.145979 0.169903 +vt 0.154861 0.177225 +vt 0.079082 0.001308 +vt 0.207895 0.001953 +vt 0.163636 0.148273 +vt 0.152183 0.149431 +vt 0.324570 0.212584 +vt 0.251728 0.326771 +vt 0.166313 0.176068 +vt 0.173627 0.167108 +vt 0.327538 0.076629 +vt 0.172518 0.155595 +vt 0.000000 0.114187 +vt 0.144869 0.158390 +vt 0.299671 0.507235 +vt 0.310980 0.508964 +vt 0.298114 0.589611 +vt 0.286607 0.587851 +vt 0.924610 0.888456 +vt 0.913206 0.887237 +vt 0.918168 0.808593 +vt 0.929772 0.809834 +vt 0.940607 0.663336 +vt 0.930845 0.667938 +vt 0.909935 0.595313 +vt 0.919867 0.590632 +vt 0.693787 0.763629 +vt 0.703716 0.764895 +vt 0.671902 0.828695 +vt 0.661801 0.827407 +vt 0.951435 0.661962 +vt 0.930884 0.589234 +vt 0.683972 0.757677 +vt 0.651814 0.821351 +vt 0.903702 0.888456 +vt 0.908499 0.809834 +vt 0.320294 0.510701 +vt 0.307590 0.591378 +vt 0.722592 0.478952 +vt 0.725946 0.477974 +vt 0.729925 0.507498 +vt 0.729766 0.507544 +vt 0.732816 0.242991 +vt 0.736123 0.241451 +vt 0.740113 0.269380 +vt 0.739956 0.269453 +vt 0.279899 0.574978 +vt 0.283878 0.575073 +vt 0.283878 0.603975 +vt 0.283689 0.603970 +vt 0.737749 0.507081 +vt 0.733042 0.477770 +vt 0.730123 0.477767 +vt 0.730123 0.507488 +vt 0.276127 0.576106 +vt 0.283510 0.604024 +vt 0.740311 0.241376 +vt 0.740311 0.269376 +vt 0.690465 0.892395 +vt 0.694388 0.893811 +vt 0.320294 0.414965 +vt 0.323851 0.412734 +vt 0.324205 0.412126 +vt 0.324205 0.434221 +vt 0.326518 0.348775 +vt 0.326518 0.370674 +vt 0.321682 0.370314 +vt 0.320294 0.368654 +vt 0.987249 0.995924 +vt 0.984034 0.997287 +vt 0.719463 0.259661 +vt 0.717858 0.255759 +vt 0.732816 0.241376 +vt 0.720167 0.259600 +vt 0.326516 0.326771 +vt 0.320294 0.328835 +vt 0.321675 0.327070 +vt 0.633897 0.998092 +vt 0.630789 1.000000 +vt 0.286411 0.529699 +vt 0.286024 0.532308 +vt 0.286411 0.552403 +vt 0.281463 0.530744 +vt 0.276127 0.534399 +vt 0.276514 0.531790 +vt 0.981099 0.288451 +vt 0.977153 0.288967 +vt 0.717858 0.263558 +vt 0.732816 0.277864 +vt 0.326354 0.391578 +vt 0.326354 0.412126 +vt 0.321330 0.411542 +vt 0.320294 0.410747 +vt 0.697596 0.895090 +vt 0.285130 0.555116 +vt 0.286359 0.574978 +vt 0.281858 0.553211 +vt 0.276127 0.556733 +vt 0.277357 0.554020 +vt 0.991373 0.995230 +vt 0.713984 0.254138 +vt 0.714687 0.254077 +vt 0.326634 0.370674 +vt 0.326634 0.391578 +vt 0.322158 0.390641 +vt 0.320294 0.389542 +vt 0.973600 0.290658 +vt 0.286607 0.507784 +vt 0.286607 0.510227 +vt 0.280158 0.529699 +vt 0.281367 0.507509 +vt 0.276127 0.509677 +vt 0.276127 0.507235 +vt 0.638049 0.997539 +vt 0.722630 0.511027 +vt 0.722592 0.510301 +vt 0.727654 0.508923 +vt 0.732754 0.528777 +vt 0.732716 0.507544 +vt 0.732754 0.508271 +s off +f 89/1 2/2 4/3 90/4 +f 90/5 4/6 6/7 91/8 +f 91/8 6/7 8/9 92/10 +f 92/11 8/12 10/13 93/14 +f 93/14 10/13 12/15 94/16 +f 94/17 12/18 14/19 95/20 +f 121/21 126/22 116/23 117/24 +f 96/25 16/26 2/2 89/1 +f 95/20 14/19 16/27 96/28 +f 1/29 17/30 24/31 3/32 +f 49/33 18/34 23/35 50/36 +f 57/37 19/38 22/39 58/40 +f 65/41 20/42 21/43 66/44 +f 3/45 24/46 28/47 5/48 +f 50/49 23/50 27/51 51/52 +f 58/53 22/54 26/55 59/56 +f 66/57 21/58 25/59 67/60 +f 5/48 28/47 32/61 7/62 +f 51/52 27/51 31/63 52/64 +f 59/56 26/55 30/65 60/66 +f 67/60 25/59 29/67 68/68 +f 7/69 32/70 36/71 9/72 +f 52/73 31/74 35/75 53/76 +f 60/77 30/78 34/79 61/80 +f 68/81 29/82 33/83 69/84 +f 9/72 36/71 40/85 11/86 +f 53/76 35/75 39/87 54/88 +f 61/80 34/79 38/89 62/90 +f 69/84 33/83 37/91 70/92 +f 11/93 40/94 44/95 13/96 +f 54/97 39/98 43/99 55/100 +f 62/101 38/102 42/103 63/104 +f 70/105 37/106 41/107 71/108 +f 15/109 48/110 17/30 1/29 +f 56/111 47/112 18/34 49/33 +f 64/113 46/114 19/38 57/37 +f 72/115 45/116 20/42 65/41 +f 13/96 44/95 48/117 15/118 +f 55/100 43/99 47/119 56/120 +f 63/104 42/103 46/121 64/122 +f 71/108 41/107 45/123 72/124 +f 17/125 49/126 50/127 24/128 +f 24/128 50/127 51/129 28/130 +f 28/130 51/129 52/131 32/132 +f 32/132 52/131 53/133 36/134 +f 36/134 53/133 54/135 40/136 +f 40/136 54/135 55/137 44/138 +f 48/139 56/140 49/126 17/125 +f 44/138 55/137 56/140 48/139 +f 18/141 57/142 58/143 23/144 +f 23/144 58/143 59/145 27/146 +f 27/146 59/145 60/147 31/148 +f 31/148 60/147 61/149 35/150 +f 35/150 61/149 62/151 39/152 +f 39/152 62/151 63/153 43/154 +f 47/155 64/156 57/142 18/141 +f 43/154 63/153 64/156 47/155 +f 19/157 65/158 66/159 22/160 +f 22/160 66/159 67/161 26/162 +f 26/162 67/161 68/163 30/164 +f 30/164 68/163 69/165 34/166 +f 34/166 69/165 70/167 38/168 +f 38/168 70/167 71/169 42/170 +f 46/171 72/172 65/158 19/157 +f 42/170 71/169 72/172 46/171 +f 20/173 73/174 74/175 21/176 +f 21/176 74/175 75/177 25/178 +f 25/178 75/177 76/179 29/180 +f 29/180 76/179 77/181 33/182 +f 33/182 77/181 78/183 37/184 +f 37/184 78/183 79/185 41/186 +f 45/187 80/188 73/174 20/173 +f 41/186 79/185 80/188 45/187 +f 73/189 81/190 82/191 74/192 +f 74/193 82/194 83/195 75/196 +f 75/196 83/195 84/197 76/198 +f 76/199 84/200 85/201 77/202 +f 77/203 85/204 86/205 78/206 +f 78/206 86/205 87/207 79/208 +f 80/209 88/210 81/190 73/189 +f 79/208 87/207 88/211 80/212 +f 81/213 89/214 90/215 82/216 +f 82/216 90/215 91/217 83/218 +f 83/218 91/217 92/219 84/220 +f 84/220 92/219 93/221 85/222 +f 85/222 93/221 94/223 86/224 +f 86/224 94/223 95/225 87/226 +f 88/227 96/228 89/214 81/213 +f 87/226 95/225 96/228 88/227 +f 15/229 1/230 97/231 104/232 +f 5/233 7/234 100/235 99/236 +f 11/237 13/238 103/239 102/240 +f 7/234 9/241 101/242 100/235 +f 1/230 3/243 98/244 97/231 +f 13/238 15/229 104/232 103/239 +f 3/243 5/233 99/236 98/244 +f 9/241 11/237 102/240 101/242 +f 100/245 101/246 106/247 105/248 +f 104/249 97/250 108/251 107/252 +f 99/253 100/254 105/255 109/256 +f 103/257 104/258 107/259 110/260 +f 98/261 99/253 109/256 111/262 +f 102/263 103/257 110/260 112/264 +f 97/250 98/265 111/266 108/251 +f 101/246 102/267 112/268 106/247 +f 127/269 132/270 113/271 114/272 +f 133/273 138/274 119/275 120/276 +f 139/277 127/278 114/279 115/280 +f 142/281 121/21 117/24 118/282 +f 132/270 133/283 120/284 113/271 +f 126/285 139/277 115/280 116/286 +f 138/274 142/287 118/288 119/275 +f 10/13 8/12 124/289 123/290 +f 123/291 124/292 125/293 122/294 +f 122/295 125/296 126/297 121/298 +f 4/3 2/2 130/299 129/300 +f 129/301 130/302 131/303 128/304 +f 128/305 131/295 132/306 127/307 +f 16/27 14/19 136/308 135/309 +f 135/310 136/311 137/312 134/313 +f 134/313 137/312 138/314 133/315 +f 6/7 4/6 129/316 141/317 +f 141/318 129/301 128/304 140/319 +f 140/320 128/321 127/322 139/323 +f 12/15 10/13 123/290 144/324 +f 144/312 123/325 122/326 143/327 +f 143/327 122/326 121/328 142/329 +f 2/2 16/26 135/330 130/299 +f 130/302 135/331 134/332 131/303 +f 131/333 134/334 133/335 132/336 +f 8/9 6/7 141/317 124/337 +f 124/338 141/339 140/340 125/341 +f 125/341 140/340 139/342 126/343 +f 14/19 12/18 144/344 136/308 +f 136/345 144/346 143/347 137/348 +f 137/348 143/347 142/349 138/350 diff --git a/christmastree/textures/christmastree_outdoor256.png b/christmastree/textures/christmastree_outdoor256.png new file mode 100644 index 0000000..e3b8f7e Binary files /dev/null and b/christmastree/textures/christmastree_outdoor256.png differ diff --git a/christmastree/textures/christmastree_outdoor_snow256.png b/christmastree/textures/christmastree_outdoor_snow256.png new file mode 100644 index 0000000..a7a8d82 Binary files /dev/null and b/christmastree/textures/christmastree_outdoor_snow256.png differ diff --git a/christmastree/textures/indoor-tree_UV256.png b/christmastree/textures/indoor-tree_UV256.png new file mode 100644 index 0000000..1f08df9 Binary files /dev/null and b/christmastree/textures/indoor-tree_UV256.png differ diff --git a/christmastree/textures/wreath_UV256.png b/christmastree/textures/wreath_UV256.png new file mode 100644 index 0000000..43900ef Binary files /dev/null and b/christmastree/textures/wreath_UV256.png differ diff --git a/mail/.github/workflows/luacheck.yml b/mail/.github/workflows/luacheck.yml new file mode 100644 index 0000000..d00f53a --- /dev/null +++ b/mail/.github/workflows/luacheck.yml @@ -0,0 +1,17 @@ +name: luacheck + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: apt + run: sudo apt-get install -y luarocks + - name: luacheck install + run: luarocks install --local luacheck + - name: luacheck run + run: $HOME/.luarocks/bin/luacheck ./ diff --git a/mail/.github/workflows/test.yml b/mail/.github/workflows/test.yml new file mode 100644 index 0000000..f181b6e --- /dev/null +++ b/mail/.github/workflows/test.yml @@ -0,0 +1,17 @@ +name: test + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + matrix: + ENGINE_VERSION: [5.0.1, 5.1.1, 5.2.0, 5.3.0, 5.4.1, 5.5.1, 5.6.1, 5.7.0, latest] + + steps: + - uses: actions/checkout@v3 + - name: test + run: docker-compose up --exit-code-from sut diff --git a/mail/.gitignore b/mail/.gitignore new file mode 100644 index 0000000..e4faeef --- /dev/null +++ b/mail/.gitignore @@ -0,0 +1,2 @@ +i18n.py +locale/*.tr.old diff --git a/mail/.luacheckrc b/mail/.luacheckrc new file mode 100644 index 0000000..3b22541 --- /dev/null +++ b/mail/.luacheckrc @@ -0,0 +1,20 @@ +globals = { + "mail", +} + +read_globals = { + -- Stdlib + string = {fields = {"split"}}, + table = {fields = {"copy", "getn"}}, + + -- Minetest + "minetest", + "vector", "ItemStack", + "dump", + + -- Deps + "unified_inventory", "default", + + -- optional mods + "mtt", "canonical_name" +} diff --git a/mail/LICENSE b/mail/LICENSE index 745bdb1..88baf62 100644 --- a/mail/LICENSE +++ b/mail/LICENSE @@ -1,12 +1,10 @@ + The file textures/mail_button.png was created by bas080 and is licensed under the WTFPL. All other files: Copyright (c) 2016 Carter Kolwey ("Cheapie Systems") - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and/or any 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. + diff --git a/mail/README b/mail/README deleted file mode 100644 index e9ed9e6..0000000 --- a/mail/README +++ /dev/null @@ -1,26 +0,0 @@ -Mail mod for Minetest -===================== - -This mod adds a system to Minetest that allows players to send each other messages within the game. Unlike chat messages or /msgs, these can be sent to an offline player and will still be present the next time they join. - -License: See the "LICENSE" file - -Dependencies: None, but will integrate with unified_inventory if it is present. - -Crafting: N/A - -Instructions: - -The inbox can be accessed by using the /mail command or by pressing the "Mail" button in unified_inventory if that mod is installed. In that window, all messages that the player currently has have the sender and subject (truncated if necessary) shown in a list. Unread messages are shown in red, while read messages are shown in white. The "Mark Read" and "Mark Unread" buttons will change this status, as well as viewing the message. - -To view a message, either single-click on it and press "Read", or just double-click on it. A window will then open showing the sender, subject, and body of the message, with buttons to return to the inbox, reply to the message, forward it, or delete it. - -Single-clicking a message and pressing the "delete" button will remove the message from the inbox. - -The compose window can be opened by pressing the "New Message" button in the inbox. This opens a window where the recipient, subject, and body can be entered, along with buttons to cancel or send the message. Since messages can be sent to any name, including ones that have not yet joined the server for the first time, there is no validation to ensure that the recipient exists. - -If a message is sent to a player that is currently online, they will see a notification in the chat that a message has arrived, along with the sender and subject (truncated if necessary) of the message and a brief reminder that they can use the /mail command (or the mail button, if applicable) to view the message. - -If a player has unread messages in their inbox when they join, a notification will appear to notify them of this. It offers the option to either go to the inbox now, or not. If they choose not to, a reminder is shown in the chat to remind them that they can use the /mail command (or the mail button, if applicable) to view the message(s) later. - -All activity (sending messages, marking them as read/unread, deleting them, etc.) is immediately saved to a file called "mail.db" in the world directory. This file is then read at server startup. If an error occurs while saving this file, a message is printed in the server logs. diff --git a/mail/README.md b/mail/README.md new file mode 100644 index 0000000..5605587 --- /dev/null +++ b/mail/README.md @@ -0,0 +1,117 @@ +Mail mod for Minetest (ingame mod) +====== + +![](https://github.com/mt-mods/mail/workflows/test/badge.svg) +![](https://github.com/mt-mods/mail/workflows/luacheck/badge.svg) +[![ContentDB](https://content.minetest.net/packages/mt-mods/mail/shields/downloads/)](https://content.minetest.net/packages/mt-mods/mail/) + +This is a fork of @cheapie's mail mod. + +It adds a mail-system that allows players to send each other messages in-game and via webmail (optional). + +# Screenshot + +![](screenshot_1.2.0.png) + +# Installation + +## In-game mail mod + +Install it like any other mod: copy the directory `mail_mod` to your "worldmods" folder or use the [ContentDB](https://content.minetest.net) + +## Webmail + +To provide a web-based interface to receive/send mails you can use the [mtui](https://github.com/minetest-go/mtui) project. + +# Commands/Howto + +To access your mail type `/mail` command or click on the mail button in your inventory (`unified_inventory`). + +Mails can be deleted, marked as read or unread, replied to and forwarded to another player. You can also manage your contacts and your mailing lists. + +# Features + +* Inbox page +* Outbox page +* Saved drafts +* Read/unread marks +* To/Cc/Bcc system +* Intuitive UI +* Contacts book +* Mailing lists +* Sorters/filters (new in 1.1.0) +* Multiple selection (new in 1.1.0) +* Settings +* Chat, on join, HUD and sound notifications +* Translated in : English, French, German, Chinese (both traditional and simplified), Spanish, Brazilian Portuguese. + +# Compatibility / Migration + +Overview: +* `v1` all the data is in the `/mails.db` file +* `v2` every player has its own (in-) mailbox in the `/mails/.json` file +* `v3` every player has an entry in the `` `mod_storage/` (inbox, outbox, drafts, contacts, mailing lists, settings) + +# Dependencies +* None + +# License + +See the "LICENSE" file + +# Textures +* textures/email_mail.png (https://github.com/rubenwardy/email.git WTFPL) + +# Contributors / Credits + +* Cheapie (Initial idea/project) +* Rubenwardy (Lua/UI improvements) +* BuckarooBanzay (Clean-ups, Refactoring) +* Athozus (Outbox, Maillists, UI, Drafts, Trash, Settings) +* SX (Various fixes, UI) +* fluxionary (Minor fixups) +* Toby1710 (UX fixes) +* Peter Nerlich (CC, BCC) +* Emojigit (Traditional Chinese translation) +* Niklp09 (German translation) +* Dennis Jenkins (UX fixes) +* Thomas Rudin (Maintenance) +* imre84 (UI fixes) +* Chache (Spanish translation) +* APercy (Brazilian Portuguese translation) +* Nuno Filipe Povoa (mail_notif.ogg - https://invent.kde.org/plasma/oxygen-sounds/-/blob/master/sounds/Oxygen-Im-Nudge.ogg) +* TheTrueBeginner (Simplified Chinese translation) +* nyomi (Hungarian translation) +* whosit (UI fixes) +* Wuzzy (German translation) + +# Contribute + +You can contribute by : +* Reporting an issue : Go to the [Issues](https://github.com/mt-mods/mail/issues) tab, click on the button **New issue** and type a short title then give many informations (Minetest version, tab where the bug occured, steps to reproduce the crash, etc.) +* Give a review on ContentDB : [Just write ;)](https://content.minetest.net/packages/mt-mods/mail/review/) +* Requesting new features : [Open an issue](https://github.com/mt-mods/mail/issues) and indicate what you need more in details. +* Adding those new features : [Open a pull request](https://github.com/mt-mods/mail/pulls), and if issue(s) are linked, ping them (#number). +* Fixing an issue : Same as before, open a pull request. +* Translate into a new language : copy `locale/template.txt` into `locale/mail..tr`, and add translated strings (syntax : `not translated=translated`). Then, open a pull request. We're also working on opening a [Weblate](https://weblate.org) (free web interface) to translate strings easier. +* Add documentation : adds `.md` (markdown) or `.txt` files and open a pull request. +* ... + +You're encouraged to create a fork of this repo, then make your changes and create a pull request when it's done. ~~If you do so, please also check "Git branches" section.~~ (obsolete). Request for merging into `master`, if needed we will push into another branch. + +If you don't have a GitHub account, you can also contact maintainers to add manually your contributions. + +# Git branches + +***Caution : might be obsolete*** + +* master : main branch, where are pushed releases and tags +* dev : for new release works, A.B.C release to A.B+1.0 +* A.B.X : for fix releases (no new features), A.B.C release to A.B.C+1 + +For a new fix release (A.B.C) : merge A.B.X branch to master +For a new release (A.B+1.0) : merge dev to master, with previous A.B.C merges (include fixes in master, prioritize dev branch in case of conflicts) + +# Old/Historic stuff +* Old forum topic: https://forum.minetest.net/viewtopic.php?t=14464 +* Old mod: https://cheapiesystems.com/git/mail/ diff --git a/mail/api.lua b/mail/api.lua new file mode 100644 index 0000000..48c398b --- /dev/null +++ b/mail/api.lua @@ -0,0 +1,157 @@ +-- see: mail.md + +-- translation +local S = minetest.get_translator("mail") + +local f = string.format + +mail.registered_on_receives = {} +function mail.register_on_receive(func) + mail.registered_on_receives[#mail.registered_on_receives + 1] = func +end + +function mail.send(m) + if type(m.from) ~= "string" then return false, "'from' is not a string" end + if type(m.to) ~= "string" then return false, "'to' is not a string" end + if type(m.body) ~= "string" then return false, "'body' is not a string" end + + -- defaults + m.subject = m.subject or "(No subject)" + + -- normalize to, cc and bcc while compiling a list of all recipients + local recipients = {} + local undeliverable = {} + m.to = mail.concat_player_list(mail.extractMaillists(m.to, m.from)) + m.to = mail.normalize_players_and_add_recipients(m.to, recipients, undeliverable) + if m.cc then + m.cc = mail.concat_player_list(mail.extractMaillists(m.cc, m.from)) + m.cc = mail.normalize_players_and_add_recipients(m.cc, recipients, undeliverable) + end + if m.bcc then + m.bcc = mail.concat_player_list(mail.extractMaillists(m.bcc, m.from)) + m.bcc = mail.normalize_players_and_add_recipients(m.bcc, recipients, undeliverable) + end + + if next(undeliverable) then -- table is not empty + local undeliverable_names = {} + for name in pairs(undeliverable) do + undeliverable_names[#undeliverable_names + 1] = '"' .. name .. '"' + end + return false, f("recipients %s don't exist; cannot send mail.", table.concat(undeliverable_names, ", ")) + end + + local extra = {} + local extra_log + if m.cc then + table.insert(extra, "CC: " .. m.cc) + end + if m.bcc then + table.insert(extra, "BCC: " .. m.bcc) + end + if #extra > 0 then + extra_log = f(" (%s)", table.concat(extra, " - ")) + else + extra_log = "" + end + + minetest.log("action", f("[mail] %q send mail to %q%s with subject %q and body %q", + m.from, m.to, extra_log, m.subject, m.body + )) + + local id + if m.id then + mail.delete_mail(m.from, m.id) + id = m.id + end + + -- form the actual mail + local msg = { + id = id or mail.new_uuid(), + from = m.from, + to = m.to, + cc = m.cc, + bcc = m.bcc, + subject = m.subject, + body = m.body, + time = os.time(), + } + + -- add in senders outbox + local entry = mail.get_storage_entry(m.from) + table.insert(entry.outbox, 1, msg) + mail.set_storage_entry(m.from, entry) + + -- add in every receivers inbox + for recipient in pairs(recipients) do + entry = mail.get_storage_entry(recipient) + table.insert(entry.inbox, msg) + mail.set_storage_entry(recipient, entry) + end + + -- notify recipients that happen to be online + local mail_alert = S("You have a new message from @1! Subject: @2", m.from, m.subject) .. + "\n" .. S("To view it, type /mail") + local unified_inventory_alert = S("You could also use the button in your inventory.") + for _, player in ipairs(minetest.get_connected_players()) do + local name = player:get_player_name() + if recipients[name] then + if mail.get_setting(name, "chat_notifications") == true then + minetest.chat_send_player(name, mail_alert) + if minetest.get_modpath("unified_inventory") then + minetest.chat_send_player(name, unified_inventory_alert) + end + end + if mail.get_setting(name, "sound_notifications") == true then + minetest.sound_play("mail_notif", {to_player=name}) + end + local receiver_entry = mail.get_storage_entry(name) + local receiver_messages = receiver_entry.inbox + mail.hud_update(name, receiver_messages) + end + end + + for i=1, #mail.registered_on_receives do + if mail.registered_on_receives[i](m) then + break + end + end + + return true +end + +function mail.save_draft(m) + if type(m.from) ~= "string" then return false, "'from' is not a string" end + if type(m.to) ~= "string" then return false, "'to' is not a string" end + if type(m.body) ~= "string" then return false, "'body' is not a string" end + + -- defaults + m.subject = m.subject or "(No subject)" + + minetest.log("verbose", f("[mail] %q saves draft with subject %q and body %q", + m.from, m.subject, m.body + )) + + -- remove it is an update + local id + if m.id then + mail.delete_mail(m.from, m.id) + id = m.id + end + + -- add (again ie. update) in sender drafts + local entry = mail.get_storage_entry(m.from) + table.insert(entry.drafts, 1, { + id = id or mail.new_uuid(), + from = m.from, + to = m.to, + cc = m.cc, + bcc = m.bcc, + subject = m.subject, + body = m.body, + time = os.time(), + }) + mail.set_storage_entry(m.from, entry) + + return true + +end diff --git a/mail/api.md b/mail/api.md new file mode 100644 index 0000000..7ef2ea9 --- /dev/null +++ b/mail/api.md @@ -0,0 +1,97 @@ + +# Mail format +The mail format in the api hooks + +```lua +mail = { + from = "sender name", + to = "players, which, are, addressed", + cc = "carbon copy", + bcc = "players, which, get, a, copy, but, are, not, visible, to, others", + subject = "subject line", + body = "mail body" +} +``` + +The fields `to`, `cc` and `bcc` can contain a player, multiple player names separated by commas, or be empty. +Players in `to` are the recipiants, who are addressed directly. `cc` specifies players that get the mail to get notified, but are not immediate part of the conversation. +There is no technical difference between `to` and `cc`, it just implies meaning for the players. +Players can see all fields making up the mail except `bcc`, which is the only difference to `cc`. + +## Sending mail + +```lua +local success, error = mail.send({ + from = "singleplayer", + to = "playername", + cc = "carbon, copy", + bcc = "blind, carbon, copy", + subject = "subject line", + body = "mail body" +}) + +-- if "success" is false the error parameter will contain a message +``` + +# Hooks +On-receive mail hook: + +```lua +mail.register_on_receive(function(m) + -- "m" is an object in the form: "Mail format" +end) +``` + +# Internals + +mod-storage entry for a player (indexed by playername and serialized with json): +```lua +{ + contacts = { + { + -- name of the player (unique key in the list) + name = "", + -- note + note = "" + },{ + ... + } + }, + inbox = { + { + -- globally unique mail id + id = "d6cce35c-487a-458f-bab2-9032c2621f38", + -- sending player name + from = "", + -- receiving player name + to = "", + -- carbon copy (optional) + cc = "playername, playername2", + -- blind carbon copy (optional) + bcc = "", + -- mail subject + subject = "", + -- mail body + body = "", + -- timestamp (os.time()) + time = 1234, + -- read-flag (true: player has read the mail, inbox only) + read = true + },{ + ... + } + }, + outbox = { + -- same format as "inbox" + }, + lists = { + { + -- name of the maillist (unique key in the list) + name = "", + -- description + description = "", + -- playername list + players = {"playername", "playername2"} + } + } +} diff --git a/mail/api.spec.lua b/mail/api.spec.lua new file mode 100644 index 0000000..a9f9d2f --- /dev/null +++ b/mail/api.spec.lua @@ -0,0 +1,12 @@ +mtt.register("send mail", function(callback) + -- send a mail + local success, err = mail.send({from = "player1", to = "player2", subject = "something", body = "blah"}) + assert(success) + assert(not err) + + -- check the receivers inbox + local entry = mail.get_storage_entry("player2") + assert(entry) + assert(#entry.inbox > 0) + callback() +end) diff --git a/mail/chatcommands.lua b/mail/chatcommands.lua new file mode 100644 index 0000000..a152274 --- /dev/null +++ b/mail/chatcommands.lua @@ -0,0 +1,10 @@ +minetest.register_chatcommand("mail",{ + description = "Open the mail interface", + func = function(name, param) + if #param > 0 then -- if param is not empty + mail.show_compose(name, param) -- make a new message + else + mail.show_mail_menu(name) -- show main menu + end + end +}) diff --git a/mail/depends.txt b/mail/depends.txt deleted file mode 100644 index 20b7c42..0000000 --- a/mail/depends.txt +++ /dev/null @@ -1 +0,0 @@ -unified_inventory? diff --git a/mail/docker-compose.yml b/mail/docker-compose.yml new file mode 100644 index 0000000..f7f9976 --- /dev/null +++ b/mail/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.6" + +services: + sut: + build: + context: ./test + args: + ENGINE_VERSION: ${ENGINE_VERSION:-5.7.0} + user: root + volumes: + - "./:/root/.minetest/worlds/world/worldmods/mail/" + - "world_data:/root/.minetest/worlds/world" + - "./test/world.mt:/root/.minetest/worlds/world/world.mt" + - "./test/minetest.conf:/minetest.conf" + ports: + - "30000:30000/udp" + +volumes: + world_data: {} \ No newline at end of file diff --git a/mail/gui.lua b/mail/gui.lua new file mode 100644 index 0000000..3287360 --- /dev/null +++ b/mail/gui.lua @@ -0,0 +1,12 @@ + +if minetest.get_modpath("unified_inventory") then + + unified_inventory.register_button("mail", { + type = "image", + image = "mail_button.png", + tooltip = "Mail", + action = function(player) + mail.show_mail_menu(player:get_player_name()) + end + }) +end diff --git a/mail/hud.lua b/mail/hud.lua new file mode 100644 index 0000000..7eca2a2 --- /dev/null +++ b/mail/hud.lua @@ -0,0 +1,59 @@ + +local huddata = {} + +minetest.register_on_joinplayer(function(player) + local name = player:get_player_name() + local data = {} + + data.imageid = player:hud_add({ + hud_elem_type = "image", + name = "MailIcon", + position = {x=0.52, y=0.52}, + text="", + scale = {x=1,y=1}, + alignment = {x=0.5, y=0.5}, + }) + + data.textid = player:hud_add({ + hud_elem_type = "text", + name = "MailText", + position = {x=0.55, y=0.52}, + text= "", + scale = {x=1,y=1}, + alignment = {x=0.5, y=0.5}, + }) + + + huddata[name] = data +end) + +minetest.register_on_leaveplayer(function(player) + local name = player:get_player_name() + huddata[name] = nil +end) + + +function mail.hud_update(playername, messages) + local data = huddata[playername] + local player = minetest.get_player_by_name(playername) + + if not data or not player then + return + end + + local unreadcount = 0 + for _, message in ipairs(messages) do + if not message.read then + unreadcount = unreadcount + 1 + end + end + + if unreadcount == 0 or (not mail.get_setting(playername, "hud_notifications")) then + player:hud_change(data.imageid, "text", "") + player:hud_change(data.textid, "text", "") + else + player:hud_change(data.imageid, "text", "email_mail.png") + player:hud_change(data.textid, "text", unreadcount .. " /mail") + end + +end diff --git a/mail/init.lua b/mail/init.lua index 71895e3..4df95a0 100644 --- a/mail/init.lua +++ b/mail/init.lua @@ -1,226 +1,101 @@ -mail = {} +mail = { + -- version + version = 3, -mail.highlightedmessages = {} + -- mod storage + storage = minetest.get_mod_storage(), -mail.messages = {} + -- ui theme prepend + theme = "", -function mail.load() - local file = io.open(minetest.get_worldpath().."/mail.db","r") - if file then - local data = file:read("*a") - mail.messages = minetest.deserialize(data) - file:close() - end + -- ui forms + ui = {}, + + -- per-user ephemeral data + selected_idxs = { + inbox = {}, + outbox = {}, + drafts = {}, + trash = {}, + contacts = {}, + maillists = {}, + to = {}, + cc = {}, + bcc = {}, + boxtab = {}, + sortfield = {}, + sortdirection = {}, + filter = {}, + multipleselection = {}, + optionstab = {}, + chat_notifications = {}, + onjoin_notifications = {}, + hud_notifications = {}, + sound_notifications = {}, + unreadcolorenable = {}, + cccolorenable = {}, + trash_move_enable = {}, + auto_marking_read = {}, + }, + + colors = { + header = "#999", + selected = "#72FF63", + important = "#FFD700", + additional = "#CCCCDD", + imp_sel = "#B9EB32", + add_sel = "#9FE6A0", + imp_add = "#E6D26F", + imp_add_sel = "#BFE16B", + highlighted = "#608631", + new = "#00F529" + }, + + message_drafts = {} +} + +if minetest.get_modpath("default") then + mail.theme = default.gui_bg .. default.gui_bg_img end -function mail.save() - local file = io.open(minetest.get_worldpath().."/mail.db","w") - if file and file:write(minetest.serialize(mail.messages)) and file:close() then - return true - else - minetest.log("error","[mail] Save failed - messages may be lost!") - return false - end +-- sub files +local MP = minetest.get_modpath(minetest.get_current_modname()) +dofile(MP .. "/util/normalize.lua") +dofile(MP .. "/util/contact.lua") +dofile(MP .. "/util/uuid.lua") +dofile(MP .. "/util/time_ago.lua") +dofile(MP .. "/chatcommands.lua") +dofile(MP .. "/migrate.lua") +dofile(MP .. "/hud.lua") +dofile(MP .. "/storage.lua") +dofile(MP .. "/api.lua") +dofile(MP .. "/gui.lua") +dofile(MP .. "/onjoin.lua") +dofile(MP .. "/ui/mail.lua") +dofile(MP .. "/ui/inbox.lua") +dofile(MP .. "/ui/outbox.lua") +dofile(MP .. "/ui/drafts.lua") +dofile(MP .. "/ui/trash.lua") +dofile(MP .. "/ui/message.lua") +dofile(MP .. "/ui/receivers.lua") +dofile(MP .. "/ui/events.lua") +dofile(MP .. "/ui/contacts.lua") +dofile(MP .. "/ui/edit_contact.lua") +dofile(MP .. "/ui/select_contact.lua") +dofile(MP .. "/ui/maillists.lua") +dofile(MP .. "/ui/edit_maillists.lua") +dofile(MP .. "/ui/compose.lua") +dofile(MP .. "/ui/options.lua") +dofile(MP .. "/ui/settings.lua") +dofile(MP .. "/ui/about.lua") + +-- migrate storage +mail.migrate() + +if minetest.get_modpath("mtt") then + dofile(MP .. "/mtt.lua") + dofile(MP .. "/api.spec.lua") + dofile(MP .. "/migrate.spec.lua") + dofile(MP .. "/util/uuid.spec.lua") + dofile(MP .. "/util/normalize.spec.lua") end - -mail.inboxformspec = "size[8,9;]".. - "button_exit[7.5,0;0.5,0.5;quit;X]".. - "button[6.25,1;1.5,0.5;new;New Message]".. - "button[6.25,2;1.5,0.5;read;Read]".. - "button[6.25,3;1.5,0.5;reply;Reply]".. - "button[6.25,4;1.5,0.5;forward;Forward]".. - "button[6.25,5;1.5,0.5;delete;Delete]".. - "button[6.25,6;1.5,0.5;markread;Mark Read]".. - "button[6.25,7;1.5,0.5;markunread;Mark Unread]".. - "button[6.25,8;1.5,0.5;about;About]".. - "textlist[0,0.5;6,8.5;message;" - -function mail.send(src,dst,subject,body) - if not mail.messages[dst] then mail.messages[dst] = {} end - table.insert(mail.messages[dst],1,{unread=true,sender=src,subject=subject,body=body}) - for _,player in ipairs(minetest.get_connected_players()) do - local name = player:get_player_name() - if name == dst then - if subject == "" then subject = "(No subject)" end - minetest.chat_send_player(dst,string.format("You have a new message from %s!. Use the /mail command" .. (minetest.get_modpath("unified_inventory") and " or the mail button in the inventory " or " ") .. "to view it. Subject: %s",src,(string.len(subject) > 30 and string.sub(subject,1,27) .. "..." or subject))) - end - end - mail.save() -end - -function mail.showabout(name) - local formspec = "size[4,5;]".. - "button[3.5,0;0.5,0.5;back;X]".. - "label[0,0;Mail]".. - "label[0,0.5;By cheapie]".. - "label[0,1;http://github.com/cheapie/mail]".. - "label[0,1.5;See LICENSE file for license information]".. - "label[0,2.5;NOTE: Communication using this system]".. - "label[0,3;is NOT guaranteed to be private!]".. - "label[0,3.5;Admins are able to view the messages]".. - "label[0,4;of any player.]" - minetest.show_formspec(name,"mail:about",formspec) -end - -function mail.showinbox(name) - local formspec = mail.inboxformspec - if not mail.messages[name] then mail.messages[name] = {} end - local idx, message - if mail.messages[name][1] then - for idx,message in ipairs(mail.messages[name]) do - if idx ~= 1 then formspec = formspec .. "," end - if message.unread then - formspec = formspec .. "#FF8888" - end - formspec = formspec .. "From: " .. minetest.formspec_escape(message.sender) .. " Subject: " - if message.subject ~= "" then - if string.len(message.subject) > 30 then - formspec = formspec .. minetest.formspec_escape(string.sub(message.subject,1,27)).. "..." - else - formspec = formspec .. minetest.formspec_escape(message.subject) - end - else - formspec = formspec .. "(No subject)" - end - end - formspec = formspec .. "]label[0,0;Welcome! You've got mail!]" - else - formspec = formspec .. "No mail :(]label[0,0;Welcome!]" - end - minetest.show_formspec(name,"mail:inbox",formspec) -end - -function mail.showmessage(name,msgnumber) - local message = mail.messages[name][msgnumber] - local formspec = "size[8,6]button[7.5,0;0.5,0.5;back;X]label[0,0;From: %s]label[0,0.5;Subject: %s]textarea[0.25,1;8,4;body;;%s]button[1,5;2,1;reply;Reply]button[3,5;2,1;forward;Forward]button[5,5;2,1;delete;Delete]" - local sender = minetest.formspec_escape(message.sender) - local subject = minetest.formspec_escape(message.subject) - local body = minetest.formspec_escape(message.body) - formspec = string.format(formspec,sender,subject,body) - minetest.show_formspec(name,"mail:message",formspec) -end - -function mail.showcompose(name,defaulttgt,defaultsubj,defaultbody) - local formspec = "size[8,8]field[0.25,0.5;4,1;to;To:;%s]field[0.25,1.5;4,1;subject;Subject:;%s]textarea[0.25,2.5;8,4;body;;%s]button[1,7;2,1;cancel;Cancel]button[7.5,0;0.5,0.5;cancel;X]button[5,7;2,1;send;Send]" - formspec = string.format(formspec,minetest.formspec_escape(defaulttgt),minetest.formspec_escape(defaultsubj),minetest.formspec_escape(defaultbody)) - minetest.show_formspec(name,"mail:compose",formspec) -end - -minetest.register_on_player_receive_fields(function(player,formname,fields) - if formname == "mail:about" then - mail.showinbox(player:get_player_name()) - elseif formname == "mail:inbox" then - local name = player:get_player_name() - if fields.message then - local event = minetest.explode_textlist_event(fields.message) - mail.highlightedmessages[name] = event.index - if event.type == "DCL" and mail.messages[name][mail.highlightedmessages[name]] then - mail.messages[name][mail.highlightedmessages[name]].unread = false - mail.showmessage(name,mail.highlightedmessages[name]) - end - end - if fields.read then - if mail.messages[name][mail.highlightedmessages[name]] then - mail.messages[name][mail.highlightedmessages[name]].unread = false - mail.showmessage(name,mail.highlightedmessages[name]) - end - elseif fields.delete then - if mail.messages[name][mail.highlightedmessages[name]] then table.remove(mail.messages[name],mail.highlightedmessages[name]) end - mail.showinbox(name) - mail.save() - elseif fields.reply and mail.messages[name][mail.highlightedmessages[name]] then - local message = mail.messages[name][mail.highlightedmessages[name]] - local replyfooter = "Type your reply here."..string.char(10)..string.char(10).."--Original message follows--"..string.char(10)..message.body - mail.showcompose(name,message.sender,"Re: "..message.subject,replyfooter) - elseif fields.forward and mail.messages[name][mail.highlightedmessages[name]] then - local message = mail.messages[name][mail.highlightedmessages[name]] - local fwfooter = "Type your message here."..string.char(10)..string.char(10).."--Original message follows--"..string.char(10)..message.body - mail.showcompose(name,"","Fw: "..message.subject,fwfooter) - elseif fields.markread then - if mail.messages[name][mail.highlightedmessages[name]] then mail.messages[name][mail.highlightedmessages[name]].unread = false end - mail.showinbox(name) - mail.save() - elseif fields.markunread then - if mail.messages[name][mail.highlightedmessages[name]] then mail.messages[name][mail.highlightedmessages[name]].unread = true end - mail.showinbox(name) - mail.save() - elseif fields.new then - mail.showcompose(name,"","","Type your message here.") - elseif fields.quit then - if minetest.get_modpath("unified_inventory") then - unified_inventory.set_inventory_formspec(player, "craft") - end - elseif fields.about then - mail.showabout(name) - end - return true - elseif formname == "mail:message" then - local name = player:get_player_name() - if fields.back then - mail.showinbox(name) - elseif fields.reply then - local message = mail.messages[name][mail.highlightedmessages[name]] - local replyfooter = "Type your reply here."..string.char(10)..string.char(10).."--Original message follows--"..string.char(10)..message.body - mail.showcompose(name,message.sender,"Re: "..message.subject,replyfooter) - elseif fields.forward then - local message = mail.messages[name][mail.highlightedmessages[name]] - local fwfooter = "Type your message here."..string.char(10)..string.char(10).."--Original message follows--"..string.char(10)..message.body - mail.showcompose(name,"","Fw: "..message.subject,fwfooter) - elseif fields.delete then - if mail.messages[name][mail.highlightedmessages[name]] then table.remove(mail.messages[name],mail.highlightedmessages[name]) end - mail.showinbox(name) - mail.save() - end - return true - elseif formname == "mail:compose" then - if fields.send then - mail.send(player:get_player_name(),fields.to,fields.subject,fields.body) - end - mail.showinbox(player:get_player_name()) - return true - elseif formname == "mail:unreadnag" then - if fields.yes then - mail.showinbox(player:get_player_name()) - else - minetest.chat_send_player(player:get_player_name(),"You can use the /mail command" .. (minetest.get_modpath("unified_inventory") and " or the mail button in the inventory " or " ") .. "to read your messages later.") - end - return true - elseif fields.mail then - mail.showinbox(player:get_player_name()) - else - return false - end -end) - -if minetest.get_modpath("unified_inventory") then - unified_inventory.register_button("mail", { - type = "image", - image = "mail_button.png", - tooltip = "Mail" - }) -end - -minetest.register_chatcommand("mail",{ - description = "Open the mail interface", - func = function(name) - mail.showinbox(name) - end - } -) - -minetest.register_on_joinplayer(function(player) - minetest.after(0,function(player) - local name = player:get_player_name() - local unreadflag = false - if mail.messages[name] then - for _,message in ipairs(mail.messages[name]) do - if message.unread then unreadflag = true end - end - end - if unreadflag then - minetest.show_formspec(name,"mail:unreadnag","size[3,2]label[0,0;You have unread messages in your inbox.]label[0,0.5;Go there now?]button[0.5,0.75;2,1;yes;Yes]button_exit[0.5,1.5;2,1;no;No]") - end - end,player) -end) - -mail.load() diff --git a/mail/locale/mail.de.tr b/mail/locale/mail.de.tr new file mode 100644 index 0000000..6bbb361 --- /dev/null +++ b/mail/locale/mail.de.tr @@ -0,0 +1,101 @@ +# textdomain: mail +Provided by mt-mods=Bereitgestellt von mt-mods +Version=Version +Licenses=Lizenzen +Expat (code), WTFPL (textures)=Expat (Code), WTFPL (Texturen) +Communication using this system is NOT guaranteed to be private!=Die Kommunikation über dieses System ist NICHT garantiert privat! +Admins are able to view the messages of any player.=Admins können die Nachrichten aller Spieler sehen. +Contributors=Mitwirkende +You have a new message from @1! Subject: @2=Sie haben eine neue Nachricht von @1! Betreff: @2 +To view it, type /mail=Geben Sie zum Anzeigen „/mail“ ein +You could also use the button in your inventory.=Sie können auch die Schaltfläche in Ihrem Inventar verwenden. +BCC=BCC +Cancel=Abbrechen +Save draft=Entwurf speichern +Send=Senden +No contacts=Keine Kontakte +Name=Name +No drafts=Keine Entwürfe +Edit=Bearbeiten +To=An +Player name=Spielername +That name is already in your contacts=Dieser Name ist bereits in Ihren Kontakten +The contact name cannot be empty.=Der Kontaktname kann nicht leer sein. +Note=Anmerkung +Maillist name=Verteilerlistenname +Desc=Beschreibung +Players=Spieler +That name is already in your mailing lists.=Dieser Name ist bereits in Ihren Verteilerlisten. +The mailing list name cannot be empty.=Der Verteilerlistenname kann nicht leer sein. +Back=Zurück +Mark Read=Als gelesen makieren +Mark Unread=Als ungelesen makieren +Trash=Papierkorb +Inbox=Posteingang +Outbox=Senden +Drafts=Entwürfe +Contacts=Kontakte +Mail lists=Verteilerlisten +Options=Einstellungen +Close=Schließen +(No subject)=(Kein Betreff) +Subject=Betreff +Delete=Löschen +New=Neu +Date=Datum +Filter=Filter +Allow multiple selection=Mehrfachauswahl zulassen +@1 of @2 selected=@1 von @2 ausgewählt +(Un)select all=Alle aus-/abwählen +No mail=Keine Nachrichten +Reply=Antworten +Reply all=Allen antworten +Forward=Weiterleiten +Reply only to the sender= +Reply to all involved people= +Transfer message to other people= +Read=Lesen +From=Von +(No description)=(Keine Beschreibung) +No maillist=Keine Verteilerliste +Receivers=Empfänger +You have mail! Type /mail to read=Sie haben Post! „/mail“ eingeben zum Lesen +Add=Hinzufügen +Remove=Entfernen +CC=CC +Notifications=Benachrichtigungen +Chat notifications=Chat-Benachrichtigungen +On join notifications=Bei-Eintritt-Benachrichtigungen +HUD notifications=HUD-Benachrichtigungen +Sound notifications=Klang-Benachrichtigungen +Message list=Nachrichtenliste +Show unread in different color=Ungelesenes in anderer Farbe anzeigen +Show CC/BCC in different color=CC/BCC in anderer Farbe anzeigen +Default sorting fields=Standardsortierfelder +Other=Anderes +Move deleted messages to trash=Gelöschte Nachrichten in den Papierkorb verschieben +Automatic marking read= +Date format:=Datumsformat: +Receive a message in the chat when there is a new message= +Receive a message at login when inbox isn't empty= +Show an HUD notification when inbox isn't empty= +Play a sound when there is a new message= +Mark a message as read when opened= +Reset=Zurücksetzen +Settings=Einstellungen +About=Über +Save=Speichern +Ascending=Aufsteigend +Descending=Absteigend +From/To=Von/An +years=Jahren +months=Monaten +weeks=Wochen +days=Tagen +hours=Stunden +minuts=Minuten +seconds=Sekunden +@1 ago=Vor @1 +Restore=Wiederherstellen +Empty=Leer +Trash is empty=Papierkorb ist leer diff --git a/mail/locale/mail.es.tr b/mail/locale/mail.es.tr new file mode 100644 index 0000000..7e070aa --- /dev/null +++ b/mail/locale/mail.es.tr @@ -0,0 +1,101 @@ +# textdomain: mail +Provided by mt-mods=Proporcionado por mt-mods +Version=Versión +Licenses=Licencias +Expat (code), WTFPL (textures)=Expat (código), WTFPL (texturas) +Communication using this system is NOT guaranteed to be private!=¡NO se garantiza que la comunicación mediante este sistema sea privada! +Admins are able to view the messages of any player.=Los administradores pueden ver los mensajes de cualquier jugador. +Contributors=Colaboradores +You have a new message from @1! Subject: @2=¡Tienes un nuevo mensaje de @1! Asunto: @2 +To view it, type /mail=Para verlo, escribe /mail +You could also use the button in your inventory.=También puedes usar el botón de tu inventario. +BCC=CCO +Cancel=Cancelar +Save draft=Guardar borrador +Send=Enviar +No contacts=Sin contactos +Edit=Editar +No drafts=No hay borradores +Player name=Nombre del jugador +That name is already in your contacts=Ese nombre ya está en tus contactos +The contact name cannot be empty.=El nombre de contacto no puede estar vacío. +Note=Nota +Back=Volver +Save=Guardar +Maillist name=Nombre de la lista de correo +Desc=Desc +Players=Jugadores +That name is already in your mailing lists.=Ese nombre ya está en tus listas de correo. +The mailing list name cannot be empty.=El nombre de lista de correo no puede estar vacío. +Mark Read=Marcar como leído +Mark Unread=Marcar como no leído +Reply=Responder +Reply all=Responder a todos +Forward=Reenviar +Reply only to the sender=Responder solo al remitente +Reply to all involved people=Responder a todas las personas implicadas +Transfer message to other people=Transferir el mensaje a otras personas +From=De +Filter=Filtrar +Allow multiple selection=Permitir selección múltiple +@1 of @2 selected=@1 de @2 seleccionado(s) +(Un)select all=(Des)seleccionar todos +No mail=Sin correo +Subject=Asunto +New=Nuevo +Delete=Borrar +Date=Fecha +(No description)=(Sin descripción) +No maillist=Sin lista de correo +Receivers=Recipientes +You have mail! Type /mail to read=¡Tienes correo! Escribe /mail para leerlo +Read=Leído +Trash=Papelera +Inbox=Entrada +Outbox=Enviados +Drafts=Borradores +Contacts=Contactos +Mail lists=Listas de correo +Options=Opciones +Close=Cerrar +(No subject)=(Sin asunto) +To=Para +Add=Añadir +Remove=Quitar +Name=Nombre +CC=CC +Notifications=Notificaciones +Chat notifications=Notificaciones de chat +On join notifications=Notificaciones al unirse +HUD notifications=Notificaciones de interfaz +Sound notifications=Notificaciones de sonido +Message list=Lista de mensajes +Show unread in different color=Mostrar no-leídos en diferente color +Show CC/BCC in different color=Mostrar CC/CCO en diferente color +Default sorting fields=Campos a ordenar por defecto +Other=Otros +Move deleted messages to trash=Mover mensajes borrados a la papelera +Automatic marking read=Marcar como leído automáticamente +Receive a message in the chat when there is a new message=Recibir un mensaje en el chat cuando hay correo nuevo +Receive a message at login when inbox isn't empty=Recibir mensaje al conectarse si la bandeja de entrada no está vacía +Show an HUD notification when inbox isn't empty=Mostrar una notificación en la interfaz cuando la bandeja de entrada no está vacía +Play a sound when there is a new message=Emitir un sonido cuando hay un correo nuevo +Mark a message as read when opened=Marcar un mensaje como leído al abrirlo +Reset=Restablecer +Settings=Ajustes +About=Acerca de +Ascending=Ascendente +Descending=Descendente +From/To=De/Para +years=años +months=meses +weeks=semanas +days=días +hours=horas +minuts=minutos +seconds=segundos +@1 ago=hace @1 +Restore=Restaurar +Empty=Vacío +Trash is empty=La papelera está vacía +Date format:=Formato de fecha: diff --git a/mail/locale/mail.fr.tr b/mail/locale/mail.fr.tr new file mode 100644 index 0000000..89a6dc6 --- /dev/null +++ b/mail/locale/mail.fr.tr @@ -0,0 +1,101 @@ +# textdomain: mail +Provided by mt-mods=Fourni par mt-mods +Version=Version +Licenses=Licences +Expat (code), WTFPL (textures)=Expat (code), WTFPL (textures) +Communication using this system is NOT guaranteed to be private!=La communication par ce système n'est pas garantie d'être privée ! +Admins are able to view the messages of any player.=Les administrateurs peuvent voir les messages de chaque joueur. +Contributors=Contributeurs +You have a new message from @1! Subject: @2=Vous avez un nouveau message de @1 ! Objet : @2 +To view it, type /mail=Pour le consulter, entrez /mail +You could also use the button in your inventory.=Vous pouvez également utiliser le bouton dans votre inventaire +BCC=Cci +Cancel=Annuler +Save draft=Enregistrer le brouillon +Send=Envoyer +No contacts=Aucun contact +Name=Nom +No drafts=Aucun brouillon +Edit=Modifier +To=À +Player name=Nom du joueur +That name is already in your contacts=Ce nom est déjà dans vos contacts +The contact name cannot be empty.=Le nom du contact ne peut pas être vide. +Note=Note +Maillist name=Nom de la liste de diffusion +Desc=Desc +Players=Joueurs +That name is already in your mailing lists.=Ce nom est déjà présent dans vos listes de diffusion. +The mailing list name cannot be empty.=Le nom de la liste de diffusion ne peut pas être vide. +Back=Retour +Mark Read=Marquer comme lu +Mark Unread=Marquer non lu +Trash=Corbeille +Inbox=Boîte de réception +Outbox=Envoyés +Drafts=Brouillons +Contacts=Contacts +Mail lists=Listes de diffusion +Options=Options +Close=Fermer +(No subject)=(Sans objet) +Subject=Objet +Delete=Supprimer +New=Nouveau +Date=Date +Filter=Filtre +Allow multiple selection=Autoriser la sélection multiple +@1 of @2 selected=@1 sur @2 sélectionnés +(Un)select all=Tout (dé)sélectionner +No mail=Aucun mail +Reply=Répondre +Reply all=Répondre à tous +Forward=Transférer +Reply only to the sender=Répondre uniquement à l'expéditeur +Reply to all involved people=Répondre à toutes les personnes concernées +Transfer message to other people=Transférer le message à d'autres personnes +Read=Lire +From=De +(No description)=(Sans description) +No maillist=Aucune liste de diffusion +Receivers=Destinataires +You have mail! Type /mail to read=Vous avez reçu un mail ! Entrez /mail pour le consulter +Add=Ajouter +Remove=Enlever +CC=Cc +Notifications=Notifications +Chat notifications=Notifications dans le tchat +On join notifications=Notifications à la connexion +HUD notifications=Notifications ATH +Sound notifications=Notifications sonores +Message list=Liste de messages +Show unread in different color=Coloriser les non lus +Show CC/BCC in different color=Coloriser les Cc/Cci +Default sorting fields=Champs de tri par défaut +Other=Autre +Move deleted messages to trash=Supprimer les messages dans la corbeille +Automatic marking read=Lu automatique +Date format:=Format de la date : +Receive a message in the chat when there is a new message=Recevoir un message dans le tchat lorsqu'un nouveau message est reçu +Receive a message at login when inbox isn't empty=Recevoir un message à la connexion lorsque la boîte de réception n'est pas vide +Show an HUD notification when inbox isn't empty=Indiquer dans l'ATH que la boîte de réception n'est pas vide +Play a sound when there is a new message=Jouer un son lorsqu'un nouveau message est reçu +Mark a message as read when opened=Marquer un message comme lu lorsqu'il est ouvert +Reset=Réinitialiser +Settings=Paramètres +About=À propos +Save=Sauvegarder +Ascending=Croissant +Descending=Décroissant +From/To=De/À +years=années +months=mois +weeks=semaines +days=jours +hours=heures +minuts=minutes +seconds=secondes +@1 ago=Il y a @1 +Restore=Restaurer +Empty=Vider +Trash is empty=La corbeille est vide diff --git a/mail/locale/mail.hu.tr b/mail/locale/mail.hu.tr new file mode 100644 index 0000000..4f5ea3b --- /dev/null +++ b/mail/locale/mail.hu.tr @@ -0,0 +1,101 @@ +# textdomain: mail +Provided by mt-mods=Feltéve, hogy az én mt-mod-om +Version=Verzió +Licenses=License +Expat (code), WTFPL (textures)=Expat (kód), WTFPL (textúrák) +Communication using this system is NOT guaranteed to be private!=A systemben lévő komunikáció nem garantáltan privát! +Admins are able to view the messages of any player.=Az adminok megtudják nézni minden játékos üzenetjét. +Contributors=Közreműködöttek +You have a new message from @1! Subject: @2=Van egy új üzeneted @1-től Cím: @2 +To view it, type /mail=Ahhoz hogy megnézd, írd /mail +You could also use the button in your inventory.=A gombot is tudod használni az inventoridban. +BCC=BCC +Cancel=Mégse +Save draft=mentés piszkozatként +Send=Küldés +No contacts= +Name=Név +No drafts=Nincsenek piszkozatok +Edit=Szerkesztés +To=Neki +Player name=Játékos neve +That name is already in your contacts=A név már a kontaktok között van +The contact name cannot be empty.=A contakt neve nem lehet üres. +Note=Jegyzet +Maillist name=Levelező lista neve +Desc=Desc +Players=Játékosok +That name is already in your mailing lists.=A név már benne van a levelező listában +The mailing list name cannot be empty.=A levelező lista neve nem lehet üres +Back=Visza +Mark Read=Jelöld olvasottként +Mark Unread=Jelöld olvasatlanul +Trash= +Inbox=PostaLáda +Outbox=Elküldött +Drafts=Piszkozatok +Contacts=Contaktok +Mail lists=Levelező lista +Options=Lehetőségek +Close=Bezár +(No subject)=(nincs cím) +Subject=Cím +Delete=Törlés +New=Új +Date=Dátum +Filter=Filterek +Allow multiple selection= +@1 of @2 selected= +(Un)select all=(ne válaszd ki) mindegyik választása +No mail=Nincs levél +Reply=Válasz +Reply all=Válaszmindenkinek +Forward=Továbbítás +Reply only to the sender= +Reply to all involved people= +Transfer message to other people= +Read=Olvasott +From=Tőle +(No description)=(Nincs leírás) +No maillist=Nincs levelező lista +Receivers= +You have mail! Type /mail to read=Van egy leveled! Írd /mail az olvasáshoz +Add=Hozzáadás +Remove=Elvétel +CC=CC +Notifications=Értesítések +Chat notifications=Chates értesítések +On join notifications=Belépési értesírés +HUD notifications= HUD értesítés +Sound notifications=Hang értesítés +Message list=Üzenetek listája +Show unread in different color=Mutasd a nem olvasottakat más színnel +Show CC/BCC in different color=Mutasd a CC-t/BCC-t más színnel +Default sorting fields=Alap válogató terület +Other= +Move deleted messages to trash= +Automatic marking read= +Date format:= +Receive a message in the chat when there is a new message= +Receive a message at login when inbox isn't empty= +Show an HUD notification when inbox isn't empty= +Play a sound when there is a new message= +Mark a message as read when opened= +Reset=Viszaállítás +Settings=Beállítások +About=Róla +Save=Mentés +Ascending=Emelkedő +Descending=Sűlyedő +From/To=Tól(től)/neki +years= +months= +weeks= +days= +hours= +minuts= +seconds= +@1 ago= +Restore= +Empty= +Trash is empty= diff --git a/mail/locale/mail.pt_BR.tr b/mail/locale/mail.pt_BR.tr new file mode 100644 index 0000000..7c5d47b --- /dev/null +++ b/mail/locale/mail.pt_BR.tr @@ -0,0 +1,101 @@ +# textdomain: mail +Provided by mt-mods= +Version= +Licenses= +Expat (code), WTFPL (textures)= +Communication using this system is NOT guaranteed to be private!=A comunicação usando este sistema não possui garantia de privacidade +Admins are able to view the messages of any player.=Administradores poderão ler as mensagens de qualquer jogador +Contributors= +You have a new message from @1! Subject: @2=Você tem uma mensagem de @1! Assunto: @2 +To view it, type /mail=Para visualizar a mensagem, digite /mail +You could also use the button in your inventory.=Você também pode usar o botão do seu inventário. +BCC=BCC +Cancel=Cancelar +Save draft=Salvar rascunho +Send=Enviar +No contacts= +Name=Nome +No drafts=Sem rascunhos +Edit=Editar +To=Para +Player name=Nome do jogador +That name is already in your contacts=Esse nome já consta em sua lista de contatos +The contact name cannot be empty.=Informe o nome do destinatário +Note=Nota +Maillist name=Nome da lista de discussão +Desc=Descrição +Players=Jogador +That name is already in your mailing lists.=Esse nome ja está sendo usado em sua lista de discussões +The mailing list name cannot be empty.=O nome da lista de discussões deve ser informado +Back=Voltar +Mark Read=Marcar como lido +Mark Unread=Marcar como não lido +Trash= +Inbox=Entrada +Outbox=Enviadas +Drafts=Rascunhos +Contacts=Contatos +Mail lists=Lista de correios +Options=Opções +Close=Fechar +(No subject)=(Sem assunto) +Subject=Assunto +Delete=Apagar +New=Novo +Date=Data +Filter=Filtrar +Allow multiple selection=Permitir selecionar vários +@1 of @2 selected= +(Un)select all=Desmarcar todos +No mail=Sem e-mails no momento +Reply=Responder +Reply all=Responder Todos +Forward=Encaminhar +Reply only to the sender= +Reply to all involved people= +Transfer message to other people= +Read=Ler +From=De +(No description)=(sem descrição) +No maillist=Sem lista de discussão +Receivers= +You have mail! Type /mail to read=Você recebeu e-mail! Tecle /mail para ler +Add=Adicionar +Remove=Remover +CC=CC +Notifications=Notificações +Chat notifications=Notificação de conversa +On join notifications=Notificação ao entrar +HUD notifications=Notificação no HUD +Sound notifications= +Message list=Lista de mensagens +Show unread in different color=Exibir mensagens não lidas em uma cor diferente +Show CC/BCC in different color=Exibir mensagens com copia em uma cor diferente +Default sorting fields=Ordenamento de campos padrão +Other= +Move deleted messages to trash= +Automatic marking read= +Date format:= +Receive a message in the chat when there is a new message= +Receive a message at login when inbox isn't empty= +Show an HUD notification when inbox isn't empty= +Play a sound when there is a new message= +Mark a message as read when opened= +Reset= +Settings=Ajustes +About=Sobre +Save=Salvar +Ascending=Ascendente +Descending=Descendente +From/To=De/Para +years= +months= +weeks= +days= +hours= +minuts= +seconds= +@1 ago= +Restore= +Empty= +Trash is empty= diff --git a/mail/locale/mail.zh_CN.tr b/mail/locale/mail.zh_CN.tr new file mode 100644 index 0000000..c75cf82 --- /dev/null +++ b/mail/locale/mail.zh_CN.tr @@ -0,0 +1,102 @@ +# textdomain: mail +Provided by mt-mods= +Version= +Licenses= +Expat (code), WTFPL (textures)= +Communication using this system is NOT guaranteed to be private!= +Admins are able to view the messages of any player.= +Contributors= +You have a new message from @1! Subject: @2= +To view it, type /mail= +You could also use the button in your inventory.= +BCC=密送 +Cancel=取消 +Save draft=保存草稿 +Send=发送 +No contacts= +Name=名字 +#if new means new mail, it would be New=新邮件 +No drafts=没有草稿 +Edit=编辑 +To=收件人 +Player name=玩家名字 +That name is already in your contacts= +The contact name cannot be empty.= +Note=备注 +Maillist name=建组名 +Desc=描述 +Players=玩家 +That name is already in your mailing lists.= +The mailing list name cannot be empty.= +Back=返回 +Mark Read=标记为已读 +Mark Unread=标记为未读 +Trash= +Inbox=收件箱 +Outbox=已发送 +Drafts=草稿 +Contacts=通讯录 +Mail lists=建组 +Options= +Close=关闭 +(No subject)=(无主题) +Subject=主题 +Delete=删除 +New=新 +Date=时间 +Filter=筛选 +Allow multiple selection=允许多选 +@1 of @2 selected= +(Un)select all=(取消)选中所有 +No mail=无邮件 +Reply=回复 +Reply all=回复所有 +Forward=转发 +Reply only to the sender= +Reply to all involved people= +Transfer message to other people= +Read=浏览 +From=发件人 +(No description)=(无描述) +No maillist=无建组 +Receivers= +You have mail! Type /mail to read= +Add=添加 +Remove=移除 +CC=抄送 +Notifications= +Chat notifications= +On join notifications= +HUD notifications= +Sound notifications= +Message list= +Show unread in different color= +Show CC/BCC in different color= +Default sorting fields= +Other= +Move deleted messages to trash= +Automatic marking read= +Date format:= +Receive a message in the chat when there is a new message= +Receive a message at login when inbox isn't empty= +Show an HUD notification when inbox isn't empty= +Play a sound when there is a new message= +Mark a message as read when opened= +Reset= +Settings= +About=关于 +Save=保存 +Ascending=升序 +Descending=降序 +From/To= +years= +months= +weeks= +days= +hours= +minuts= +seconds= +@1 ago= +Restore= +Empty= +Trash is empty= diff --git a/mail/locale/mail.zh_TW.tr b/mail/locale/mail.zh_TW.tr new file mode 100644 index 0000000..d5df3a2 --- /dev/null +++ b/mail/locale/mail.zh_TW.tr @@ -0,0 +1,101 @@ +# textdomain: mail +Provided by mt-mods= +Version= +Licenses= +Expat (code), WTFPL (textures)= +Communication using this system is NOT guaranteed to be private!= +Admins are able to view the messages of any player.= +Contributors= +You have a new message from @1! Subject: @2= +To view it, type /mail= +You could also use the button in your inventory.= +BCC=密件副本 +Cancel=取消 +Save draft=儲存草稿 +Send=發送 +No contacts= +Name=名稱 +No drafts=沒有草稿 +Edit=編輯 +To=收件人 +Player name=玩家名稱 +That name is already in your contacts= +The contact name cannot be empty.= +Note=備註 +Maillist name=郵件列表名稱 +Desc=描述 +Players=玩家 +That name is already in your mailing lists.= +The mailing list name cannot be empty.= +Back=返回 +Mark Read=標記已讀 +Mark Unread=標記未讀 +Trash= +Inbox=收件箱 +Outbox=寄件備份 +Drafts=草稿 +Contacts=聯繫人 +Mail lists=郵件列表 +Options= +Close=關閉 +(No subject)=(沒有主旨) +Subject=主旨 +Delete=刪除 +New=新建 +Date=日期 +Filter= +Allow multiple selection= +@1 of @2 selected= +(Un)select all= +No mail=沒有郵件 +Reply=回覆 +Reply all=回覆所有人 +Forward=轉寄 +Reply only to the sender= +Reply to all involved people= +Transfer message to other people= +Read=閱讀 +From=寄件者 +(No description)=(沒有描述) +No maillist=沒有郵件列表 +Receivers= +You have mail! Type /mail to read= +Add=加入 +Remove=移除 +CC=副本 +Notifications= +Chat notifications= +On join notifications= +HUD notifications= +Sound notifications= +Message list= +Show unread in different color= +Show CC/BCC in different color= +Default sorting fields= +Other= +Move deleted messages to trash= +Automatic marking read= +Date format:= +Receive a message in the chat when there is a new message= +Receive a message at login when inbox isn't empty= +Show an HUD notification when inbox isn't empty= +Play a sound when there is a new message= +Mark a message as read when opened= +Reset= +Settings= +About=關於 +Save=儲存 +Ascending= +Descending= +From/To= +years= +months= +weeks= +days= +hours= +minuts= +seconds= +@1 ago= +Restore= +Empty= +Trash is empty= diff --git a/mail/locale/template.txt b/mail/locale/template.txt new file mode 100644 index 0000000..ed940e1 --- /dev/null +++ b/mail/locale/template.txt @@ -0,0 +1,101 @@ +# textdomain: mail +Provided by mt-mods= +Version= +Licenses= +Expat (code), WTFPL (textures)= +Communication using this system is NOT guaranteed to be private!= +Admins are able to view the messages of any player.= +Contributors= +You have a new message from @1! Subject: @2= +To view it, type /mail= +You could also use the button in your inventory.= +BCC= +Cancel= +Save draft= +Send= +No contacts= +Name= +No drafts= +Edit= +To= +Player name= +That name is already in your contacts= +The contact name cannot be empty.= +Note= +Maillist name= +Desc= +Players= +That name is already in your mailing lists.= +The mailing list name cannot be empty.= +Back= +Mark Read= +Mark Unread= +Trash= +Inbox= +Outbox= +Drafts= +Contacts= +Mail lists= +Options= +Close= +(No subject)= +Subject= +Delete= +New= +Date= +Filter= +Allow multiple selection= +@1 of @2 selected= +(Un)select all= +No mail= +Reply= +Reply all= +Forward= +Reply only to the sender= +Reply to all involved people= +Transfer message to other people= +Read= +From= +(No description)= +No maillist= +Receivers= +You have mail! Type /mail to read= +Add= +Remove= +CC= +Notifications= +Chat notifications= +On join notifications= +HUD notifications= +Sound notifications= +Message list= +Show unread in different color= +Show CC/BCC in different color= +Default sorting fields= +Other= +Move deleted messages to trash= +Automatic marking read= +Date format:= +Receive a message in the chat when there is a new message= +Receive a message at login when inbox isn't empty= +Show an HUD notification when inbox isn't empty= +Play a sound when there is a new message= +Mark a message as read when opened= +Reset= +Settings= +About= +Save= +Ascending= +Descending= +From/To= +years= +months= +weeks= +days= +hours= +minuts= +seconds= +@1 ago= +Restore= +Empty= +Trash is empty= diff --git a/mail/mail.hu.tr b/mail/mail.hu.tr new file mode 100644 index 0000000..622925f --- /dev/null +++ b/mail/mail.hu.tr @@ -0,0 +1,76 @@ +# textdomain: mail +# author: nyomi +Provided by mt-mods=Feltéve, hogy az én mt-mod-om +Version=Verzió +Licenses=License +Expat (code), WTFPL (textures)=Expat (kód), WTFPL (textúrák) +Communication using this system is NOT guaranteed to be private!=A systemben lévő komunikáció nem garantáltan privát! +Admins are able to view the messages of any player.=Az adminok megtudják nézni minden játékos üzenetjét. +Contributors=Közreműködöttek +Note=Jegyzet +Settings=Beállítások +About=Róla +You have a new message from @1! Subject: @2=Van egy új üzeneted @1-től Cím: @2 +To view it, type /mail=Ahhoz hogy megnézd, írd /mail +You could also use the button in your inventory.=A gombot is tudod használni az inventoridban. +BCC=BCC +Cancel=Mégse +Save draft=mentés piszkozatként +Send=Küldés +Subject=Cím +To=Neki +CC=CC +Name=Név +No drafts=Nincsenek piszkozatok +Edit=Szerkesztés +New=Új +Delete=Törlés +Inbox=PostaLáda +Outbox=Elküldött üzenetek +Drafts=Piszkozatok +Contacts=Contaktok +Mail lists=Levelező lista +Options=Lehetőségek +Close=Bezár +(No subject)=(nincs cím) +Player name=Játékos neve +That name is already in your contacts=A név már a kontaktok között van +The contact name cannot be empty.=A contakt neve nem lehet üres. +Save=Mentés +Maillist name=Levelező lista neve +Desc=Desc +Players=Játékosok +That name is already in your mailing lists.=A név már benne van a levelező listában +The mailing list name cannot be empty.=A levelező lista neve nem lehet üres +Mark Read=Jelöld olvasottként +Mark Unread=Jelöld olvasatlanul +From=Tőle +Read=Olvasott +Filter=Filterek +Allow multiple selection= +@1 selected=@1 kiválasztva +(Un)select all=(ne válaszd ki) mindegyik választása +No mail=Nincs levél +Reply=Válasz +Reply all=Válaszmindenkinek +Forward=Továbbítás +Date=Dátum +Ascending=Emelkedő +Descending=Sűlyedő +(No description)=(Nincs leírás) +No maillist=Nincs levelező lista +You have mail! Type /mail to read=Van egy leveled! Írd /mail az olvasáshoz +Add=Hozzáadás +Remove=Elvétel +Back=Visza +Notifications=Értesítések +Chat notifications=Chates értesítések +On join notifications=Belépési értesírés +HUD notifications= HUD értesítés +Sound notifications=Hang értesítés +Message list=Üzenetek listája +Show unread in different color=Mutasd a nem olvasottakat más színnel +Show CC/BCC in different color=Mutasd a CC-t/BCC-t más színnel +Default sorting fields=Alap válogató terület +From/To=Tól(től)/neki +Reset=Viszaállítás diff --git a/mail/migrate.lua b/mail/migrate.lua new file mode 100644 index 0000000..0266e63 --- /dev/null +++ b/mail/migrate.lua @@ -0,0 +1,99 @@ + +local STORAGE_VERSION_KEY = "@@version" + +local function migrate_v1_to_v3() + local file = io.open(minetest.get_worldpath().."/mail.db", "r") + assert(file) + print("[mail] Migration from v1 to v3 database") + + local data = file:read("*a") + local oldmails = minetest.deserialize(data) + file:close() + + for name, oldmessages in pairs(oldmails) do + print("[mail,v1] + migrating player '" .. name .. "'") + local entry = mail.get_storage_entry(name) + for _, msg in ipairs(oldmessages) do + table.insert(entry.inbox, { + id = mail.new_uuid(), + from = msg.sender or msg.from, + to = msg.to or name, + subject = msg.subject, + body = msg.body, + time = msg.time or os.time(), + }) + end + mail.set_storage_entry(name, entry) + end + + -- rename file + print("[mail,v1] migration done, renaming old mail.db") + os.rename(minetest.get_worldpath().."/mail.db", minetest.get_worldpath().."/mail.db.old") +end + +local function read_json_file(path) + local file = io.open(path, "r") + local content = {} + if file then + local json = file:read("*a") + content = minetest.parse_json(json or "[]") or {} + file:close() + end + return content +end + +-- migrate from v2 to v3 database +local function migrate_v2_to_v3() + local maildir = minetest.get_worldpath().."/mails" + minetest.mkdir(maildir) -- if necessary (eg. first login) + print("[mail] Migration from v2 to v3 database") + + -- defer execution until auth-handler ready (first server-step) + minetest.after(0, function() + for playername, _ in minetest.get_auth_handler().iterate() do + local entry = mail.get_storage_entry(playername) + + local player_contacts = read_json_file(maildir .. "/contacts/" .. playername .. ".json") + for _, c in pairs(player_contacts) do + table.insert(entry.contacts, { name = c.name, note = c.note }) + end + + local saneplayername = string.gsub(playername, "[.|/]", "") + local player_inbox = read_json_file(maildir .. "/" .. saneplayername .. ".json") + print("[mail,v2] + migrating player '" .. playername .. "'") + for _, msg in ipairs(player_inbox) do + table.insert(entry.inbox, { + id = mail.new_uuid(), + from = msg.sender or msg.from, + to = msg.to or playername, + cc = msg.cc, + subject = msg.subject, + body = msg.body, + time = msg.time or os.time(), + read = not msg.unread, + }) + end + + mail.set_storage_entry(playername, entry) + end + print("[mail,v2] migration done") + end) +end + +function mail.migrate() + -- check for v2 storage first, v1-migration might have set the v3-flag already + local version = mail.storage:get_int(STORAGE_VERSION_KEY) + if version < 3 then + -- v2 to v3 + migrate_v2_to_v3() + mail.storage:set_int(STORAGE_VERSION_KEY, 3) + end + + -- check for v1 storage + local v1_file = io.open(minetest.get_worldpath().."/mail.db", "r") + if v1_file then + -- v1 to v3 + migrate_v1_to_v3() + mail.storage:set_int(STORAGE_VERSION_KEY, 3) + end +end diff --git a/mail/migrate.spec.lua b/mail/migrate.spec.lua new file mode 100644 index 0000000..b98341b --- /dev/null +++ b/mail/migrate.spec.lua @@ -0,0 +1,28 @@ + +mtt.register("migrate v1", function(callback) + local entry = mail.get_storage_entry("old_v1_player") + assert(entry) + assert(#entry.inbox == 1) + assert(entry.inbox[1].from == "singleplayer") + assert(entry.inbox[1].to == "old_v1_player") + assert(entry.inbox[1].subject == "test1") + assert(entry.inbox[1].body == "test2") + assert(entry.inbox[1].id) + assert(entry.inbox[1].time > 0) + + callback() +end) + +mtt.register("migrate v2", function(callback) + local entry = mail.get_storage_entry("old_v2_player") + assert(entry) + assert(#entry.inbox == 1) + assert(entry.inbox[1].from == "someone-else") + assert(entry.inbox[1].to == "old_v2_player") + assert(entry.inbox[1].subject == "test1") + assert(entry.inbox[1].body == "test2") + assert(entry.inbox[1].id) + assert(entry.inbox[1].time == 1678467148) + + callback() +end) \ No newline at end of file diff --git a/mail/mod.conf b/mail/mod.conf new file mode 100644 index 0000000..6a57a3d --- /dev/null +++ b/mail/mod.conf @@ -0,0 +1,6 @@ +name = mail +description = ingame mail-system +optional_depends = canonical_name,default,mtt,unified_inventory +release = 19819 +author = mt-mods +title = Mail diff --git a/mail/mtt.lua b/mail/mtt.lua new file mode 100644 index 0000000..7c8cf0b --- /dev/null +++ b/mail/mtt.lua @@ -0,0 +1,10 @@ + +mtt.register("setup", function(callback) + -- create test players + local auth_handler = minetest.get_auth_handler() + auth_handler.set_password("player1", "") + auth_handler.set_password("player2", "") + auth_handler.set_password("player3", "") + + callback() +end) \ No newline at end of file diff --git a/mail/onjoin.lua b/mail/onjoin.lua new file mode 100644 index 0000000..f7a45e2 --- /dev/null +++ b/mail/onjoin.lua @@ -0,0 +1,23 @@ +-- translation +local S = minetest.get_translator("mail") + +minetest.register_on_joinplayer(function(player) + minetest.after(2, function(name) + local entry = mail.get_storage_entry(name) + local messages = entry.inbox + mail.hud_update(name, messages) + + local unreadcount = 0 + + for _, message in pairs(messages) do + if not message.read then + unreadcount = unreadcount + 1 + end + end + + if unreadcount > 0 and mail.get_setting(name, "onjoin_notifications") then + minetest.chat_send_player(name, + minetest.colorize(mail.colors.new, "(" .. unreadcount .. ") " .. S("You have mail! Type /mail to read"))) + end + end, player:get_player_name()) +end) diff --git a/mail/screenshot_1.1.0.png b/mail/screenshot_1.1.0.png new file mode 100644 index 0000000..0ff4759 Binary files /dev/null and b/mail/screenshot_1.1.0.png differ diff --git a/mail/screenshot_1.2.0.png b/mail/screenshot_1.2.0.png new file mode 100644 index 0000000..c0cfbae Binary files /dev/null and b/mail/screenshot_1.2.0.png differ diff --git a/mail/screenshot_1.png b/mail/screenshot_1.png new file mode 100644 index 0000000..3d34306 Binary files /dev/null and b/mail/screenshot_1.png differ diff --git a/mail/screenshot_2.png b/mail/screenshot_2.png new file mode 100644 index 0000000..d33c90a Binary files /dev/null and b/mail/screenshot_2.png differ diff --git a/mail/sounds/mail_notif.ogg b/mail/sounds/mail_notif.ogg new file mode 100644 index 0000000..7d45a89 Binary files /dev/null and b/mail/sounds/mail_notif.ogg differ diff --git a/mail/storage.lua b/mail/storage.lua new file mode 100644 index 0000000..dc954e2 --- /dev/null +++ b/mail/storage.lua @@ -0,0 +1,453 @@ +-- storage getter/setter +local STORAGE_PREFIX = "mail/" + +-- create or populate empty fields on an entry +local function populate_entry(e) + e = e or {} + e.contacts = e.contacts or {} + e.inbox = e.inbox or {} + e.outbox = e.outbox or {} + e.drafts = e.drafts or {} + e.trash = e.trash or {} + e.lists = e.lists or {} + e.settings = e.settings or {} + return e +end + +local cache = {} + +-- retrieve the storage entry for the player +function mail.get_storage_entry(playername) + local key = STORAGE_PREFIX .. playername + if cache[key] then + -- use cached entry + return cache[key] + end + + local str = mail.storage:get_string(key) + local entry + if str == "" then + -- new entry + entry = populate_entry() + else + -- deserialize existing entry + local e = minetest.parse_json(str) + entry = populate_entry(e) + end + + -- cache for next time + cache[key] = entry + return entry +end + +-- entries queued for saving +local save_queued_entries = {} + +-- save the storage entry for the player +function mail.set_storage_entry(playername, entry) + local key = STORAGE_PREFIX .. playername + -- cache + cache[key] = entry + -- enqueue for writing + save_queued_entries[key] = entry +end + +local function save_worker() + for key, entry in pairs(save_queued_entries) do + -- write to backend + mail.storage:set_string(key, minetest.write_json(entry)) + end + + -- clear queue + save_queued_entries = {} + + -- save every second + minetest.after(1, save_worker) +end + +-- start save-worker loop +save_worker() +-- save on shutdown +minetest.register_on_shutdown(save_worker) + +-- get a mail by id from the players in- or outbox +function mail.get_message(playername, msg_id) + local entry = mail.get_storage_entry(playername) + for _, msg in ipairs(entry.inbox) do + if msg.id == msg_id then + return msg + end + end + for _, msg in ipairs(entry.outbox) do + if msg.id == msg_id then + return msg + end + end + for _, msg in ipairs(entry.drafts) do + if msg.id == msg_id then + return msg + end + end + for _, msg in ipairs(entry.trash) do + if msg.id == msg_id then + return msg + end + end +end + +-- get player boxes where a message appears +function mail.get_message_boxes(playername, msg_id) + local entry = mail.get_storage_entry(playername) + local boxes = {} + for _, msg in ipairs(entry.inbox) do + if msg.id == msg_id then + table.insert(boxes, "inbox") + end + end + for _, msg in ipairs(entry.outbox) do + if msg.id == msg_id then + table.insert(boxes, "outbox") + end + end + for _, msg in ipairs(entry.drafts) do + if msg.id == msg_id then + table.insert(boxes, "drafts") + end + end + for _, msg in ipairs(entry.trash) do + if msg.id == msg_id then + table.insert(boxes, "trash") + end + end + return boxes +end + +local function safe_find(str, sub) + return str and sub and str:find(sub, 1, true) or nil +end + +function mail.sort_messages(messages, sortfield, descending, filter) + local results = {} + -- Filtering + if filter and filter ~= "" then + for _, msg in ipairs(messages) do + if safe_find(msg.from, filter) or safe_find(msg.to, filter) or safe_find(msg.subject, filter) then + table.insert(results, msg) + end + end + else + for i = 1, #messages do + results[i] = messages[i] + end + end + -- Sorting + if sortfield ~= nil then + if descending then + table.sort(results, function(a, b) return a[sortfield] > b[sortfield] end) + else + table.sort(results, function(a, b) return a[sortfield] < b[sortfield] end) + end + end + return results +end + +-- marks a mail read by its id +function mail.mark_read(playername, msg_ids) + local entry = mail.get_storage_entry(playername) + if type(msg_ids) ~= "table" then -- if this is not a table + msg_ids = { msg_ids } + end + for _, read_msg_id in ipairs(msg_ids) do + for _, entry_msg in ipairs(entry.inbox) do + if entry_msg.id == read_msg_id then + entry_msg.read = true + end + end + end + mail.set_storage_entry(playername, entry) + mail.hud_update(playername, entry.inbox) + return +end + +-- marks a mail unread by its id +function mail.mark_unread(playername, msg_ids) + local entry = mail.get_storage_entry(playername) + if type(msg_ids) ~= "table" then -- if this is not a table + msg_ids = { msg_ids } + end + for _, unread_msg_id in ipairs(msg_ids) do + for _, entry_msg in ipairs(entry.inbox) do + if entry_msg.id == unread_msg_id then + entry_msg.read = false + end + end + end + mail.set_storage_entry(playername, entry) + return +end + +-- deletes a mail by its id +function mail.delete_mail(playername, msg_ids, delete_in_trash) + local entry = mail.get_storage_entry(playername) + if type(msg_ids) ~= "table" then -- if this is not a table + msg_ids = { msg_ids } + end + for i = #entry.inbox, 1, -1 do + for _, deleted_msg in ipairs(msg_ids) do + if entry.inbox[i].id == deleted_msg then + table.remove(entry.inbox, i) + break + end + end + end + for i = #entry.outbox, 1, -1 do + for _, deleted_msg in ipairs(msg_ids) do + if entry.outbox[i].id == deleted_msg then + table.remove(entry.outbox, i) + break + end + end + end + for i = #entry.drafts, 1, -1 do + for _, deleted_msg in ipairs(msg_ids) do + if entry.drafts[i].id == deleted_msg then + table.remove(entry.drafts, i) + break + end + end + end + if delete_in_trash then + for i = #entry.trash, 1, -1 do + for _, deleted_msg in ipairs(msg_ids) do + if entry.trash[i].id == deleted_msg then + table.remove(entry.trash, i) + break + end + end + end + end + mail.set_storage_entry(playername, entry) + mail.hud_update(playername, entry.inbox) + return +end + +-- move to trash mails by id +function mail.trash_mail(playername, msg_ids) + local entry = mail.get_storage_entry(playername) + if type(msg_ids) ~= "table" then -- if this is not a table + msg_ids = { msg_ids } + end + for _, id in ipairs(msg_ids) do + local msg = mail.get_message(playername, id) + msg.previous_boxes = mail.get_message_boxes(playername, id) + table.insert(entry.trash, 1, msg) + end + mail.set_storage_entry(playername, entry) + mail.delete_mail(playername, msg_ids) + return +end + +-- restore a mail from trash +function mail.restore_mail(playername, msg_id) + local entry = mail.get_storage_entry(playername) + for i, msg in ipairs(entry.trash) do + if msg.id == msg_id then + -- not anymore store previous boxes in json + local previous_boxes = msg.previous_boxes + msg.previous_boxes = nil + -- restore it in all previous boxes + for _, box in ipairs(previous_boxes) do + table.insert(entry[box], msg) + end + -- then delete it from trash + table.remove(entry.trash, i) + end + end + mail.set_storage_entry(playername, entry) + return +end + +-- clear the trash +function mail.empty_trash(playername) + local entry = mail.get_storage_entry(playername) + entry.trash = {} + mail.set_storage_entry(playername, entry) + return +end + +-- add or update a contact +function mail.update_contact(playername, contact) + local entry = mail.get_storage_entry(playername) + local existing_updated = false + for i, existing_contact in ipairs(entry.contacts) do + if existing_contact.name == contact.name then + -- update + entry.contacts[i] = contact + existing_updated = true + break + end + end + if not existing_updated then + -- insert + table.insert(entry.contacts, contact) + end + mail.set_storage_entry(playername, entry) +end + +-- deletes a contact +function mail.delete_contact(playername, contactname) + local entry = mail.get_storage_entry(playername) + for i, existing_contact in ipairs(entry.contacts) do + if existing_contact.name == contactname then + -- delete + table.remove(entry.contacts, i) + mail.set_storage_entry(playername, entry) + return + end + end +end + +-- get all contacts +function mail.get_contacts(playername) + local entry = mail.get_storage_entry(playername) + return entry.contacts +end + +-- get a contact +function mail.get_contact(playername, contactname) + local entry = mail.get_storage_entry(playername) + for _, existing_contact in ipairs(entry.contacts) do + if existing_contact.name == contactname then + return existing_contact + end + end + return false +end + +-- returns the maillists of a player +function mail.get_maillists(playername) + local entry = mail.get_storage_entry(playername) + return entry.lists +end + +-- returns the maillists of a player +function mail.get_maillist_by_name(playername, listname) + local entry = mail.get_storage_entry(playername) + for _, list in ipairs(entry.lists) do + if list.name == listname then + return list + end + end +end + +-- updates or creates a maillist +function mail.update_maillist(playername, list, old_list_name) + local entry = mail.get_storage_entry(playername) + for i, existing_list in ipairs(entry.lists) do + if existing_list.name == old_list_name then + -- delete + table.remove(entry.lists, i) + break + end + end + -- insert + table.insert(entry.lists, list) + mail.set_storage_entry(playername, entry) +end + +function mail.delete_maillist(playername, listname) + local entry = mail.get_storage_entry(playername) + for i, list in ipairs(entry.lists) do + if list.name == listname then + -- delete + table.remove(entry.lists, i) + mail.set_storage_entry(playername, entry) + return + end + end +end + +function mail.extractMaillists(receivers_string, maillists_owner) + local receivers = mail.parse_player_list(receivers_string) -- extracted receivers + + -- extract players from mailing lists + while string.find(receivers_string, "@") do + local globalReceivers = mail.parse_player_list(receivers_string) -- receivers including maillists + receivers = {} + for _, receiver in ipairs(globalReceivers) do + local receiverInfo = receiver:split("@") -- @maillist + if receiverInfo[1] and receiver == "@" .. receiverInfo[1] then + local maillist = mail.get_maillist_by_name(maillists_owner, receiverInfo[1]) + if maillist then + for _, playername in ipairs(maillist.players) do + table.insert(receivers, playername) + end + end + else -- in case of player + table.insert(receivers, receiver) + end + end + receivers_string = mail.concat_player_list(receivers) + end + + return receivers +end + +function mail.get_setting_default_value(setting_name) + local default_values = { + chat_notifications = true, + onjoin_notifications = true, + hud_notifications = true, + sound_notifications = true, + unreadcolorenable = true, + cccolorenable = true, + defaultsortfield = 3, + defaultsortdirection = 1, + trash_move_enable = true, + auto_marking_read = true, + date_format = "%Y-%m-%d %X", + } + return default_values[setting_name] +end + +function mail.get_setting(playername, setting_name) + local entry = mail.get_storage_entry(playername) + if entry.settings[setting_name] ~= nil then + return entry.settings[setting_name] + else + return mail.get_setting_default_value(setting_name) + end +end + +-- add or update a setting +function mail.set_setting(playername, key, value) + local entry = mail.get_storage_entry(playername) + entry.settings[key] = value + mail.set_storage_entry(playername, entry) +end + +function mail.reset_settings(playername) + local entry = mail.get_storage_entry(playername) + entry.settings = {} + mail.set_storage_entry(playername, entry) +end + +function mail.pairsByKeys(t, f) + -- http://www.lua.org/pil/19.3.html + local a = {} + for n in pairs(t) do table.insert(a, n) end + table.sort(a, f) + local i = 0 -- iterator variable + local iter = function() -- iterator function + i = i + 1 + if a[i] == nil then + return nil + else + --return a[i], t[a[i]] + -- add the current position and the length for convenience + return a[i], t[a[i]], i, #a + end + end + return iter +end + diff --git a/mail/storage.spec.lua b/mail/storage.spec.lua new file mode 100644 index 0000000..1be9ea4 --- /dev/null +++ b/mail/storage.spec.lua @@ -0,0 +1,45 @@ + +mtt.register("storage", function(callback) + -- sanity checks + local playername = "player1" + local entry = mail.get_storage_entry(playername) + assert(entry) + + -- create + local contact = { + name = "other-player", + note = "my-note" + } + mail.update_contact(playername, contact) + + -- read + local contacts = mail.get_contacts(playername) + assert(#contacts == 1) + assert(contacts[1].note == contact.note) + assert(contacts[1].name == contact.name) + + -- read through api + local contacts2 = mail.get_contacts(playername) + assert(#contacts2 == 1) + assert(contacts2[1].note == contact.note) + assert(contacts2[1].name == contact.name) + + -- update + mail.update_contact(playername, { + name = contact.name, + note = "xy" + }) + + -- read updated + contacts = mail.get_contacts(playername) + assert(#contacts == 1) + assert(contacts[1].note == "xy") + assert(contacts[1].name == contact.name) + + -- delete + mail.delete_contact(playername, contact.name) + contacts = mail.get_contacts(playername) + assert(#contacts == 0) + + callback() +end) \ No newline at end of file diff --git a/mail/test/Dockerfile b/mail/test/Dockerfile new file mode 100644 index 0000000..472255d --- /dev/null +++ b/mail/test/Dockerfile @@ -0,0 +1,15 @@ +ARG ENGINE_VERSION=5.7.0 +FROM registry.gitlab.com/minetest/minetest/server:${ENGINE_VERSION} + +# copy old v1 maildb for migration testing +COPY ./mail.db /root/.minetest/worlds/world/mail.db +# copy old v2 mail-dir and auth.sqlite for migration testing +COPY ./old_v2_player.json /root/.minetest/worlds/world/mails/ +COPY ./auth.sqlite /root/.minetest/worlds/world/auth.sqlite + +USER root +RUN apk add git &&\ + mkdir -p /root/.minetest/worlds/world/worldmods/ &&\ + git clone https://github.com/BuckarooBanzay/mtt /root/.minetest/worlds/world/worldmods/mtt + +ENTRYPOINT minetestserver --config /minetest.conf \ No newline at end of file diff --git a/mail/test/auth.sqlite b/mail/test/auth.sqlite new file mode 100644 index 0000000..e6bac67 Binary files /dev/null and b/mail/test/auth.sqlite differ diff --git a/mail/test/mail.db b/mail/test/mail.db new file mode 100644 index 0000000..3104acb --- /dev/null +++ b/mail/test/mail.db @@ -0,0 +1 @@ +local _={};_[1]="singleplayer";return {old_v1_player={{unread=true,subject="test1",sender=_[1],body="test2"}},[_[1]]={}} \ No newline at end of file diff --git a/mail/test/minetest.conf b/mail/test/minetest.conf new file mode 100644 index 0000000..afea5f2 --- /dev/null +++ b/mail/test/minetest.conf @@ -0,0 +1,4 @@ +default_game = minetest_game +mg_name = v7 +mtt_enable = true +mtt_filter = mail \ No newline at end of file diff --git a/mail/test/old_v2_player.json b/mail/test/old_v2_player.json new file mode 100644 index 0000000..2bb859a --- /dev/null +++ b/mail/test/old_v2_player.json @@ -0,0 +1 @@ +[{"body":"test2","sender":"someone-else","subject":"test1","time":1678467148,"unread":false}] \ No newline at end of file diff --git a/mail/test/world.mt b/mail/test/world.mt new file mode 100644 index 0000000..f03d6e6 --- /dev/null +++ b/mail/test/world.mt @@ -0,0 +1,9 @@ +enable_damage = false +creative_mode = true +mod_storage_backend = sqlite3 +auth_backend = sqlite3 +player_backend = dummy +backend = dummy +gameid = minetest +world_name = mapblock_lib +server_announce = false diff --git a/mail/textures/email_mail.png b/mail/textures/email_mail.png new file mode 100644 index 0000000..b1e5553 Binary files /dev/null and b/mail/textures/email_mail.png differ diff --git a/mail/ui/about.lua b/mail/ui/about.lua new file mode 100644 index 0000000..c6b3cfa --- /dev/null +++ b/mail/ui/about.lua @@ -0,0 +1,69 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:about" + +function mail.show_about(name) + local formspec = [[ + size[10,6;] + tabheader[0.3,1;optionstab;]] .. S("Settings") .. "," .. S("About") .. [[;2;false;false] + button[9.35,0;0.75,0.5;back;X] + label[0,0.8;Mail] + label[0,1.2;]] .. S("Provided by mt-mods") .. [[] + label[0,1.6;]] .. S("Version") .. [[ : 1.3.0] + label[0,2.2;]] .. S("Licenses") .. [[ :] + label[0.2,2.6;]] .. S("Expat (code), WTFPL (textures)") .. [[] + label[0,3.2;https://github.com/mt-mods/mail] + label[0,3.6;https://content.minetest.net/packages/mt-mods/mail] + textarea[0.5,4.8;4,5.5;;]] .. S("Note") .. [[;]] .. + S("Communication using this system is NOT guaranteed to be private!") .. " " .. + S("Admins are able to view the messages of any player.") .. [[] + + tablecolumns[color;text;text] + table[5,0.75;4.9,5.5;contributors;]] .. + mail.colors.header .. [[,]] .. S("Contributors") .. [[,,]] .. + mail.colors.important .. [[,Cheapie,Initial idea/project,]] .. + [[,Rubenwardy,Lua/UI improvements,]] .. + [[,BuckarooBanzay,Clean-ups\, Refactoring,]] .. + [[,Athozus,Boxes\, Maillists\, UI\, Settings,]] .. + [[,fluxionary,Minor fixups,]] .. + [[,SX,Various fixes\, UI,]] .. + [[,Toby1710,UX fixes,]] .. + [[,Peter Nerlich,CC\, BCC,]] .. + [[,Niklp,German translation,]] .. + [[,Emojigit,Traditional Chinese trans.,]] .. + [[,Dennis Jenkins,UX fixes,]] .. + [[,Thomas Rudin,Maintenance,]] .. + [[,NatureFreshMilk,Maintenance,]] .. + [[,imre84,UI fixes,]] .. + [[,Chache,Spanish translation,]] .. + [[,APercy,Brazilian Portuguese trans.,]] .. + [[,Nuno Filipe Povoa,mail_notif.ogg,]] .. + [[,TheTrueBeginner,Simplified Chinese trans.,]] .. + [[,nyomi,Hungarian translation,]] .. + [[,whosit,UI fixes,]] .. + [[,Wuzzy,German translation] + ]] .. mail.theme + + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local playername = player:get_player_name() + + if fields.back then + mail.show_mail_menu(playername) + + elseif fields.optionstab == "1" then + mail.selected_idxs.optionstab[playername] = 1 + mail.show_settings(playername) + + elseif fields.optionstab == "2" then + mail.selected_idxs.optionstab[playername] = 2 + mail.show_about(playername) + end +end) diff --git a/mail/ui/compose.lua b/mail/ui/compose.lua new file mode 100644 index 0000000..9187d10 --- /dev/null +++ b/mail/ui/compose.lua @@ -0,0 +1,137 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:compose" +local msg_id = {} + +function mail.show_compose(name, to, subject, body, cc, bcc, id) + local formspec = [[ + size[8,9] + button[0,0;1,1;tocontacts;]] .. S("To") .. [[:] + field[1.1,0.3;3.2,1;to;;%s] + button[4,0;1,1;cccontacts;]] .. S("CC") .. [[:] + field[5.1,0.3;3.1,1;cc;;%s] + button[4,0.75;1,1;bcccontacts;]] .. S("BCC") .. [[:] + field[5.1,1.05;3.1,1;bcc;;%s] + field[0.25,2;8,1;subject;]] .. S("Subject") .. [[:;%s] + textarea[0.25,2.5;8,6;body;;%s] + button[0.1,8.5;2.5,1;cancel;]] .. S("Cancel") .. [[] + button[2.7,8.5;2.5,1;draft;]] .. S("Save draft") .. [[] + button[5.3,8.5;2.5,1;send;]] .. S("Send") .. [[] + ]] .. mail.theme + + formspec = string.format(formspec, + minetest.formspec_escape(to) or "", + minetest.formspec_escape(cc) or "", + minetest.formspec_escape(bcc) or "", + minetest.formspec_escape(subject) or "", + minetest.formspec_escape(body) or "") + + if id then + msg_id[name] = id + else + msg_id[name] = nil + end + + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + if fields.send then + local id = mail.new_uuid() + if msg_id[name] then + id = msg_id[name] + end + if (fields.to == "" and fields.cc == "" and fields.bcc == "") or fields.body == "" then + -- if mail is invalid then store it as a draft + mail.save_draft({ + id = id, + from = name, + to = fields.to, + cc = fields.cc, + bcc = fields.bcc, + subject = fields.subject, + body = fields.body + }) + mail.show_mail_menu(name) + return + end + local success, err = mail.send({ + id = id, + from = name, + to = fields.to, + cc = fields.cc, + bcc = fields.bcc, + subject = fields.subject, + body = fields.body, + }) + if not success then + minetest.chat_send_player(name, err) + return + end + + -- add new contacts if some receivers aren't registered + local contacts = mail.get_contacts(name) + local recipients = mail.parse_player_list(fields.to) + local isNew = true + for _,recipient in ipairs(recipients) do + if recipient:sub(1,1) == "@" then -- in case of maillist -- check if first char is @ + isNew = false + else + for _,contact in ipairs(contacts) do + if contact.name == recipient then + isNew = false + break + end + end + end + if isNew then + mail.update_contact(name, {name = recipient, note = ""}) + end + end + + minetest.after(0.5, function() + mail.selected_idxs.drafts[name] = nil + mail.show_mail_menu(name) + end) + + elseif fields.tocontacts or fields.cccontacts or fields.bcccontacts then + mail.message_drafts[name] = { + to = fields.to, + cc = fields.cc, + bcc = fields.bcc, + subject = fields.subject, + body = fields.body, + } + mail.show_select_contact(name, fields.to, fields.cc, fields.bcc) + + elseif fields.cancel then + mail.message_drafts[name] = nil + + mail.show_mail_menu(name) + + elseif fields.draft then + local id = mail.new_uuid() + if msg_id[name] then + id = msg_id[name] + end + mail.save_draft({ + id = id, + from = name, + to = fields.to, + cc = fields.cc, + bcc = fields.bcc, + subject = fields.subject, + body = fields.body + }) + + mail.show_mail_menu(name) + end + + return true +end) diff --git a/mail/ui/contacts.lua b/mail/ui/contacts.lua new file mode 100644 index 0000000..0ee5a29 --- /dev/null +++ b/mail/ui/contacts.lua @@ -0,0 +1,86 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:contacts" + +local contacts_formspec = "size[8,9;]" .. mail.theme .. [[ + button[6,0.10;2,0.5;new;]] .. S("New") .. [[] + button[6,0.85;2,0.5;edit;]] .. S("Edit") .. [[] + button[6,1.60;2,0.5;delete;]] .. S("Delete") .. [[] + button[6,8.25;2,0.5;back;]] .. S("Back") .. [[] + tablecolumns[color;text;text] + table[0,0;5.75,9;contacts;]] .. mail.colors.header .. "," .. S("Name") .. "," .. S("Note") + + +function mail.show_contacts(name) + local formspec = contacts_formspec .. mail.compile_contact_list(name, mail.selected_idxs.contacts[name]) + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local contacts = mail.get_contacts(name) + + if fields.contacts then + local evt = minetest.explode_table_event(fields.contacts) + for k, _, i in mail.pairsByKeys(contacts) do + if i == evt.row - 1 then + mail.selected_idxs.contacts[name] = tonumber(k) + break + end + end + if evt.type == "DCL" and contacts[mail.selected_idxs.contacts[name]] then + mail.show_edit_contact( + name, + contacts[mail.selected_idxs.contacts[name]].name, + contacts[mail.selected_idxs.contacts[name]].note + ) + end + + elseif fields.new then + mail.selected_idxs.contacts[name] = "#NEW#" + mail.show_edit_contact(name, "", "") + + elseif fields.edit and mail.selected_idxs.contacts[name] and contacts[mail.selected_idxs.contacts[name]] then + mail.show_edit_contact( + name, + contacts[mail.selected_idxs.contacts[name]].name, + contacts[mail.selected_idxs.contacts[name]].note + ) + + elseif fields.delete then + if contacts[mail.selected_idxs.contacts[name]] then + -- delete the contact and set the selected to the next in the list, + -- except if it was the last. Then determine the new last + local found = false + local last = nil + for k in mail.pairsByKeys(contacts) do + if found then + mail.selected_idxs.contacts[name] = tonumber(k) + break + elseif k == mail.selected_idxs.contacts[name] then + mail.delete_contact(name, contacts[mail.selected_idxs.contacts[name]].name) + mail.selected_idxs.contacts[name] = nil + found = true + else + last = tonumber(k) + end + end + if found and not mail.selected_idxs.contacts[name] then + -- was the last in the list, so take the previous (new last) + mail.selected_idxs.contacts[name] = last + end + end + + mail.show_contacts(name) + + elseif fields.back then + mail.show_mail_menu(name) + end + + return true +end) diff --git a/mail/ui/drafts.lua b/mail/ui/drafts.lua new file mode 100644 index 0000000..082643a --- /dev/null +++ b/mail/ui/drafts.lua @@ -0,0 +1,58 @@ +-- translation +local S = minetest.get_translator("mail") + + +function mail.show_drafts(name) + local trash_tab = "" + if mail.get_setting(name, "trash_move_enable") then + trash_tab = "," .. S("Trash") + end + + local drafts_formspec = "size[8.5,10;]" .. mail.theme .. [[ + tabheader[0.3,1;boxtab;]] .. + S("Inbox") .. "," .. S("Outbox").. "," .. S("Drafts") .. trash_tab .. [[;3;false;false] + + button[6,0.10;2.5,0.5;new;]] .. S("New") .. [[] + button[6,0.95;2.5,0.5;edit;]] .. S("Edit") .. [[] + button[6,1.70;2.5,0.5;delete;]] .. S("Delete") .. [[] + button[6,6.8;2.5,0.5;contacts;]] .. S("Contacts") .. [[] + button[6,7.6;2.5,0.5;maillists;]] .. S("Mail lists") .. [[] + button[6,8.7;2.5,0.5;options;]] .. S("Options") .. [[] + button_exit[6,9.5;2.5,0.5;quit;]] .. S("Close") .. [[] + + tablecolumns[color;text;text] + table[0,0.7;5.75,9.35;drafts;]] .. mail.colors.header .. "," .. S("To") .. "," .. S("Subject") + + local formspec = { drafts_formspec } + local entry = mail.get_storage_entry(name) + local messages = entry.drafts + + mail.message_drafts[name] = nil + + if messages[1] then + for _, message in ipairs(messages) do + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = minetest.formspec_escape(message.to) + formspec[#formspec + 1] = "," + if message.subject ~= "" then + if string.len(message.subject) > 30 then + formspec[#formspec + 1] = minetest.formspec_escape(string.sub(message.subject, 1, 27)) + formspec[#formspec + 1] = "..." + else + formspec[#formspec + 1] = minetest.formspec_escape(message.subject) + end + else + formspec[#formspec + 1] = S("(No subject)") + end + end + if mail.selected_idxs.drafts[name] then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = tostring(mail.selected_idxs.drafts[name] + 1) + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2.25,4.5;" .. S("No drafts") .. "]" + end + minetest.show_formspec(name, "mail:drafts", table.concat(formspec, "")) +end diff --git a/mail/ui/edit_contact.lua b/mail/ui/edit_contact.lua new file mode 100644 index 0000000..1eeea16 --- /dev/null +++ b/mail/ui/edit_contact.lua @@ -0,0 +1,77 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:editcontact" + +function mail.show_edit_contact(name, contact_name, note, illegal_name_hint) + local formspec = [[ + size[6,7] + button[4,6.25;2,0.5;back;]] .. S("Back") .. [[] + field[0.25,0.5;4,1;name;]] .. S("Player name") .. [[:;%s] + textarea[0.25,1.6;4,6.25;note;]] .. S("Note") .. [[:;%s] + button[4,0.10;2,1;save;]] .. S("Save") .. [[] + ]] + if illegal_name_hint == "collision" then + formspec = formspec .. [[ + textarea[4.25,1;2.5,6;;;]] .. + S("That name is already in your contacts") .. [[] + ]] + elseif illegal_name_hint == "empty" then + formspec = formspec .. [[ + textarea[4.25,1;2.5,6;;;]] .. + S("The contact name cannot be empty.") .. [[] + ]] + end + formspec = formspec .. mail.theme + formspec = string.format(formspec, + minetest.formspec_escape(contact_name or ""), + minetest.formspec_escape(note or "")) + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local contacts = mail.get_contacts(name) + + if fields.save then + if mail.selected_idxs.contacts[name] then + local contact = contacts[mail.selected_idxs.contacts[name]] or {name = ""} + if contact.name ~= fields.name or fields.name == "" then + -- name changed! + if #fields.name == 0 then + mail.show_edit_contact(name, contact.name, fields.note, "empty") + return true + + elseif mail.get_contact(name, fields.name) then + mail.show_edit_contact(name, contact.name, fields.note, "collision") + return true + + else + contact.name = fields.name + contact.note = fields.note + mail.update_contact(name, contact) + contacts[mail.selected_idxs.contacts[name]] = nil + end + end + contact.name = fields.name + contact.note = fields.note + mail.update_contact(name, contact) + + else + mail.update_contact(name, { + name = fields.name, + note = fields.note, + }) + end + mail.show_contacts(name) + + elseif fields.back then + mail.show_contacts(name) + end + + return true +end) diff --git a/mail/ui/edit_maillists.lua b/mail/ui/edit_maillists.lua new file mode 100644 index 0000000..9ad3e38 --- /dev/null +++ b/mail/ui/edit_maillists.lua @@ -0,0 +1,87 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:editmaillist" + +function mail.show_edit_maillist(playername, maillist_name, desc, players, illegal_name_hint) + local formspec = [[ + size[6,7] + button[4,6.25;2,0.5;back;]] .. S("Back") .. [[] + field[0.25,0.5;4,1;name;]] .. S("Maillist name") .. [[:;%s] + textarea[0.25,1.6;4,2;desc;]] .. S("Desc") .. [[:;%s] + textarea[0.25,3.6;4,4.25;players;]] .. S("Players") .. [[:;%s] + button[4,0.10;2,1;save;]] .. S("Save") .. [[] + ]] + if illegal_name_hint == "collision" then + formspec = formspec .. [[ + textarea[4.25,1;2.5,6;;;]] .. + S("That name is already in your mailing lists.") .. [[] + ]] + elseif illegal_name_hint == "empty" then + formspec = formspec .. [[ + textarea[4.25,1;2.5,6;;;]] .. + S("The mailing list name cannot be empty.") .. [[] + ]] + end + formspec = formspec .. mail.theme + formspec = string.format(formspec, + minetest.formspec_escape(maillist_name or ""), + minetest.formspec_escape(desc or ""), + minetest.formspec_escape(players or "")) + minetest.show_formspec(playername, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local maillists = mail.get_maillists(name) + + if fields.save then + local old_maillist = maillists[mail.selected_idxs.maillists[name]] or {name = ""} + if mail.selected_idxs.maillists[name] then + if old_maillist.name ~= fields.name or fields.name == "" then + -- name changed! + if #fields.name == 0 then + mail.show_edit_maillist(name, old_maillist.name, fields.desc, fields.players, "empty") + return true + + elseif mail.get_maillist_by_name(name, fields.name) then + mail.show_edit_maillist(name, old_maillist.name, fields.desc, fields.players, "collision") + return true + + else + mail.update_maillist(name, { + owner = name, + name = fields.name, + desc = fields.desc, + players = mail.parse_player_list(fields.players) + }, old_maillist.name) + maillists[mail.selected_idxs.maillists[name]] = nil + end + else + mail.update_maillist(name, { + owner = name, + name = fields.name, + desc = fields.desc, + players = mail.parse_player_list(fields.players) + }, old_maillist.name) + end + else + mail.update_maillist(name, { + owner = name, + name = fields.name, + desc = fields.desc, + players = mail.parse_player_list(fields.players) + }, old_maillist.name) + end + mail.show_maillists(name) + + elseif fields.back then + mail.show_maillists(name) + end + + return true +end) diff --git a/mail/ui/events.lua b/mail/ui/events.lua new file mode 100644 index 0000000..6bf0f48 --- /dev/null +++ b/mail/ui/events.lua @@ -0,0 +1,337 @@ + +-- Getter to filter and sort messages on demand +local function messageGetter(messages, sortfield, ascending, filter) + local results + return function() + if not results then + results = mail.sort_messages(messages, sortfield, ascending, filter) + end + return results + end +end + +local function nonempty(x) + return ((type(x)=="table")and(#x>0)) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= "mail:inbox" and formname ~= "mail:outbox" + and formname ~= "mail:drafts" and formname ~= "mail:trash" then + return + elseif fields.quit then + return + end + + -- Get player name and handle / convert common input fields + local name = player:get_player_name() + local filter = (fields.search and fields.filter) or mail.selected_idxs.filter[name] or "" + local sortfieldindex = tonumber(fields.sortfield or mail.selected_idxs.sortfield[name]) or 3 + local sortdirection = fields.sortdirection or mail.selected_idxs.sortdirection[name] or "1" + local inboxsortfield = ({"from","subject","time"})[sortfieldindex] + local outboxsortfield = ({"to","subject","time"})[sortfieldindex] + + -- Be sure that inbox/outbox selected idxs aren't nil + mail.selected_idxs.inbox[name] = mail.selected_idxs.inbox[name] or {} + mail.selected_idxs.outbox[name] = mail.selected_idxs.outbox[name] or {} + + -- Store common player configuration for reuse + mail.selected_idxs.sortfield[name] = sortfieldindex + mail.selected_idxs.sortdirection[name] = sortdirection + mail.selected_idxs.filter[name] = filter + if fields.multipleselection then + mail.selected_idxs.multipleselection[name] = fields.multipleselection == "true" + end + + -- Avoid several selected after disabling the multiple selection + if not mail.selected_idxs.multipleselection[name] then + mail.selected_idxs.inbox[name] = { mail.selected_idxs.inbox[name][#mail.selected_idxs.inbox[name]] } + mail.selected_idxs.outbox[name] = { mail.selected_idxs.outbox[name][#mail.selected_idxs.outbox[name]] } + end + + -- split inbox and outbox msgs for different tests + local entry = mail.get_storage_entry(name) + local messagesDrafts = entry.drafts + local messagesTrash = entry.trash + local getInbox = messageGetter(entry.inbox, inboxsortfield, sortdirection == "2", filter) + local getOutbox = messageGetter(entry.outbox, outboxsortfield, sortdirection == "2", filter) + + -- Hanmdle formspec event + if fields.inbox then -- inbox table + local evt = minetest.explode_table_event(fields.inbox) + if evt.row == 1 then -- header + if mail.selected_idxs.sortfield[name] == evt.column-1 then -- if already this field, then change direction + mail.selected_idxs.sortdirection[name] = mail.selected_idxs.sortdirection[name] == "2" and "1" or "2" + end + mail.selected_idxs.sortfield[name] = evt.column-1 -- update column + mail.show_mail_menu(name) + return + end + if mail.selected_idxs.multipleselection[name] then + if not mail.selected_idxs.inbox[name] then + mail.selected_idxs.inbox[name] = {} + end + local selected_id = 0 + if mail.selected_idxs.inbox[name] and #mail.selected_idxs.inbox[name] > 0 then + for i, selected_msg in ipairs(mail.selected_idxs.inbox[name]) do + if getInbox()[evt.row-1].id == selected_msg then + selected_id = i + table.remove(mail.selected_idxs.inbox[name], i) + break + end + end + end + if selected_id == 0 then + table.insert(mail.selected_idxs.inbox[name], getInbox()[evt.row-1].id) + end + else + mail.selected_idxs.inbox[name] = { (getInbox()[evt.row-1] or {}).id } + end + if evt.type == "DCL" and getInbox()[evt.row-1] then + mail.show_message(name, getInbox()[evt.row-1].id) + else + mail.show_mail_menu(name) + end + return true + end + + if fields.outbox then -- outbox table + local evt = minetest.explode_table_event(fields.outbox) + if evt.row == 1 then -- header + if mail.selected_idxs.sortfield[name] == evt.column-1 then -- if already this field, then change direction + mail.selected_idxs.sortdirection[name] = mail.selected_idxs.sortdirection[name] == "2" and "1" or "2" + end + mail.selected_idxs.sortfield[name] = evt.column-1 -- update column + mail.show_mail_menu(name) + return + end + if mail.selected_idxs.multipleselection[name] then + if not mail.selected_idxs.outbox[name] then + mail.selected_idxs.outbox[name] = {} + end + local selected_id = 0 + if mail.selected_idxs.outbox[name] and #mail.selected_idxs.outbox[name] > 0 then + for i, selected_msg in ipairs(mail.selected_idxs.outbox[name]) do + if getOutbox()[evt.row-1].id == selected_msg then + selected_id = i + table.remove(mail.selected_idxs.outbox[name], i) + break + end + end + end + if selected_id == 0 then + table.insert(mail.selected_idxs.outbox[name], getOutbox()[evt.row-1].id) + end + else + mail.selected_idxs.outbox[name] = { (getOutbox()[evt.row-1] or {}).id } + end + if evt.type == "DCL" and getOutbox()[evt.row-1] then + mail.show_message(name, getOutbox()[evt.row-1].id) + else + mail.show_mail_menu(name) + end + return true + end + + if fields.drafts then -- drafts table + local evt = minetest.explode_table_event(fields.drafts) + if evt.row == 1 then -- header + if mail.selected_idxs.sortfield[name] == evt.column-1 then -- if already this field, then change direction + mail.selected_idxs.sortdirection[name] = mail.selected_idxs.sortdirection[name] == "2" and "1" or "2" + end + mail.selected_idxs.sortfield[name] = evt.column-1 -- update column + mail.show_mail_menu(name) + return + end + mail.selected_idxs.drafts[name] = evt.row - 1 + if evt.type == "DCL" and messagesDrafts[mail.selected_idxs.drafts[name]] then + mail.show_compose(name, + messagesDrafts[mail.selected_idxs.drafts[name]].to, + messagesDrafts[mail.selected_idxs.drafts[name]].subject, + messagesDrafts[mail.selected_idxs.drafts[name]].body, + messagesDrafts[mail.selected_idxs.drafts[name]].cc, + messagesDrafts[mail.selected_idxs.drafts[name]].bcc, + messagesDrafts[mail.selected_idxs.drafts[name]].id + ) + end + return true + end + + if fields.trash then -- trash table + local evt = minetest.explode_table_event(fields.trash) + if evt.row == 1 then -- header + if mail.selected_idxs.sortfield[name] == evt.column-1 then -- if already this field, then change direction + mail.selected_idxs.sortdirection[name] = mail.selected_idxs.sortdirection[name] == "2" and "1" or "2" + end + mail.selected_idxs.sortfield[name] = evt.column-1 -- update column + mail.show_mail_menu(name) + return + end + mail.selected_idxs.trash[name] = evt.row - 1 + if evt.type == "DCL" and messagesTrash[mail.selected_idxs.trash[name]] then + mail.show_message(name, messagesTrash[mail.selected_idxs.trash[name]].id) + end + return true + end + + if fields.boxtab == "1" then + mail.selected_idxs.boxtab[name] = 1 + mail.show_inbox(name, sortfieldindex, sortdirection, filter) + + elseif fields.boxtab == "2" then + mail.selected_idxs.boxtab[name] = 2 + mail.show_outbox(name, sortfieldindex, sortdirection, filter) + + elseif fields.boxtab == "3" then + mail.selected_idxs.boxtab[name] = 3 + mail.show_drafts(name) + + elseif fields.boxtab == "4" then + mail.selected_idxs.boxtab[name] = 4 + mail.show_trash(name) + + elseif fields.read then + if formname == "mail:inbox" and nonempty(mail.selected_idxs.inbox[name]) then -- inbox table + mail.show_message(name, mail.selected_idxs.inbox[name][#mail.selected_idxs.inbox[name]]) + elseif formname == "mail:outbox" and nonempty(mail.selected_idxs.outbox[name]) then -- outbox table + mail.show_message(name, mail.selected_idxs.outbox[name][#mail.selected_idxs.outbox[name]]) + elseif formname == "mail:trash" and messagesTrash[mail.selected_idxs.trash[name]] then + mail.show_message(name, messagesTrash[mail.selected_idxs.trash[name]].id) + end + + elseif fields.edit then + if formname == "mail:drafts" and messagesDrafts[mail.selected_idxs.drafts[name]] then + mail.show_compose(name, + messagesDrafts[mail.selected_idxs.drafts[name]].to, + messagesDrafts[mail.selected_idxs.drafts[name]].subject, + messagesDrafts[mail.selected_idxs.drafts[name]].body, + messagesDrafts[mail.selected_idxs.drafts[name]].cc, + messagesDrafts[mail.selected_idxs.drafts[name]].bcc, + messagesDrafts[mail.selected_idxs.drafts[name]].id + ) + end + + elseif fields.delete then + local trash_enabled = mail.get_setting(name, "trash_move_enable") + if formname == "mail:inbox" and mail.selected_idxs.inbox[name] then -- inbox table + if trash_enabled then + mail.trash_mail(name, mail.selected_idxs.inbox[name]) + else + mail.delete_mail(name, mail.selected_idxs.inbox[name]) + end + mail.selected_idxs.inbox[name] = {} + elseif formname == "mail:outbox" and mail.selected_idxs.outbox[name] then -- outbox table + if trash_enabled then + mail.trash_mail(name, mail.selected_idxs.outbox[name]) + else + mail.delete_mail(name, mail.selected_idxs.outbox[name]) + end + mail.selected_idxs.outbox[name] = {} + elseif formname == "mail:drafts" and messagesDrafts[mail.selected_idxs.drafts[name]] then -- drafts table + if trash_enabled then + mail.trash_mail(name, messagesDrafts[mail.selected_idxs.drafts[name]].id) + else + mail.delete_mail(name, messagesDrafts[mail.selected_idxs.drafts[name]].id) + end + mail.selected_idxs.drafts[name] = nil + + elseif formname == "mail:trash" and messagesTrash[mail.selected_idxs.trash[name]] then -- trash table + mail.delete_mail(name, messagesTrash[mail.selected_idxs.trash[name]].id, true) + end + + mail.show_mail_menu(name, sortfieldindex, sortdirection, filter) + + elseif fields.restore then + if messagesTrash[mail.selected_idxs.trash[name]] then + mail.restore_mail(name, messagesTrash[mail.selected_idxs.trash[name]].id) + end + mail.show_mail_menu(name, sortfieldindex, sortdirection, filter) + + elseif fields.reply then + if formname == "mail:inbox" and mail.selected_idxs.inbox[name] and #mail.selected_idxs.inbox[name] > 0 then + local message = mail.get_message(name, mail.selected_idxs.inbox[name][#mail.selected_idxs.inbox[name]]) + mail.reply(name, message) + elseif + formname == "mail:outbox" and mail.selected_idxs.outbox[name] and #mail.selected_idxs.outbox[name] > 0 then + local message = mail.get_message(name, mail.selected_idxs.outbox[name][#mail.selected_idxs.outbox[name]]) + mail.reply(name, message) + end + + elseif fields.replyall then + if formname == "mail:inbox" and mail.selected_idxs.inbox[name] and #mail.selected_idxs.inbox[name] > 0 then + local message = mail.get_message(name, mail.selected_idxs.inbox[name][#mail.selected_idxs.inbox[name]]) + mail.replyall(name, message) + elseif + formname == "mail:outbox" and mail.selected_idxs.outbox[name] and #mail.selected_idxs.outbox[name] > 0 then + local message = mail.get_message(name, mail.selected_idxs.outbox[name][#mail.selected_idxs.outbox[name]]) + mail.replyall(name, message) + end + + elseif fields.forward then + if formname == "mail:inbox" and mail.selected_idxs.inbox[name] and #mail.selected_idxs.inbox[name] > 0 then + local message = mail.get_message(name, mail.selected_idxs.inbox[name][#mail.selected_idxs.inbox[name]]) + mail.forward(name, message) + elseif + formname == "mail:outbox" and mail.selected_idxs.outbox[name] and #mail.selected_idxs.outbox[name] > 0 then + local message = mail.get_message(name, mail.selected_idxs.outbox[name][#mail.selected_idxs.outbox[name]]) + mail.forward(name, message) + end + + elseif fields.markread then + if formname == "mail:inbox" and mail.selected_idxs.inbox[name] then + mail.mark_read(name, mail.selected_idxs.inbox[name]) + end + + mail.show_mail_menu(name, sortfieldindex, sortdirection, filter) + + elseif fields.markunread then + if formname == "mail:inbox" and mail.selected_idxs.inbox[name] then + mail.mark_unread(name, mail.selected_idxs.inbox[name]) + end + + mail.show_mail_menu(name, sortfieldindex, sortdirection, filter) + + elseif fields.new then + mail.show_compose(name) + + elseif fields.empty then + mail.empty_trash(name) + mail.show_mail_menu(name) + + elseif fields.contacts then + mail.show_contacts(name) + + elseif fields.maillists then + mail.show_maillists(name) + + elseif fields.options then + mail.show_options(name) + + elseif fields.selectall then + if formname == "mail:inbox" then + local selected_number = #mail.selected_idxs.inbox[name] + mail.selected_idxs.inbox[name] = {} -- reset for select, unselect and not existing + mail.selected_idxs.multipleselection[name] = true -- enable as the button were pressed + if selected_number < #getInbox() then -- then populate it if selection isn't full + for _, msg in ipairs(getInbox()) do + table.insert(mail.selected_idxs.inbox[name], msg.id) + end + end + elseif formname == "mail:outbox" then + local selected_number = #mail.selected_idxs.outbox[name] + mail.selected_idxs.outbox[name] = {} -- reset for select, unselect and not existing + mail.selected_idxs.multipleselection[name] = true -- enable as the button were pressed + if selected_number < #getOutbox() then -- then populate it if selection isn't full + for _, msg in ipairs(getOutbox()) do + table.insert(mail.selected_idxs.outbox[name], msg.id) + end + end + end + + mail.show_mail_menu(name) + + elseif fields.sortfield or fields.sortdirection or fields.filter then + mail.show_mail_menu(name, sortfieldindex, sortdirection, filter) + end + + return true +end) diff --git a/mail/ui/inbox.lua b/mail/ui/inbox.lua new file mode 100644 index 0000000..53a8e3b --- /dev/null +++ b/mail/ui/inbox.lua @@ -0,0 +1,139 @@ +-- translation +local S = minetest.get_translator("mail") + +function mail.show_inbox(name, sortfieldindex, sortdirection, filter) + sortfieldindex = tonumber(sortfieldindex or mail.selected_idxs.sortfield[name]) + or mail.get_setting(name, "defaultsortfield") or 3 + sortdirection = tostring(sortdirection or mail.selected_idxs.sortdirection[name] + or mail.get_setting(name, "defaultsortdirection") or "1") + filter = filter or mail.selected_idxs.filter[name] or "" + mail.selected_idxs.inbox[name] = mail.selected_idxs.inbox[name] or {} + + local entry = mail.get_storage_entry(name) + local sortfield = ({"from","subject","time"})[sortfieldindex] + local messages = mail.sort_messages(entry.inbox, sortfield, sortdirection == "2", filter) + + if mail.selected_idxs.inbox[name] and #mail.selected_idxs.inbox[name] > 0 then + for i, selected_msg in ipairs(mail.selected_idxs.inbox[name]) do + local is_present = false + for _, msg in ipairs(messages) do + if msg.id == selected_msg then + is_present = true + break + end + end + if not is_present then + table.remove(mail.selected_idxs.inbox[name], i) + end + end + end + + local trash_tab = "" + if mail.get_setting(name, "trash_move_enable") then + trash_tab = "," .. S("Trash") + end + + local inbox_formspec = "size[8.5,10;]" .. mail.theme .. [[ + tabheader[0.3,1;boxtab;]] .. + S("Inbox") .. "," .. S("Outbox").. "," .. S("Drafts") .. trash_tab .. [[;1;false;false] + + button[6,0.10;2.5,0.5;new;]] .. S("New") .. [[] + button[6,0.95;2.5,0.5;read;]] .. S("Read") .. [[] + button[6,1.70;2.5,0.5;reply;]] .. S("Reply") .. [[] + button[6,2.45;2.5,0.5;replyall;]] .. S("Reply all") .. [[] + button[6,3.20;2.5,0.5;forward;]] .. S("Forward") .. [[] + button[6,3.95;2.5,0.5;delete;]] .. S("Delete") .. [[] + button[6,4.82;2.5,0.5;markread;]] .. S("Mark Read") .. [[] + button[6,5.55;2.5,0.5;markunread;]] .. S("Mark Unread") .. [[] + button[6,6.8;2.5,0.5;contacts;]] .. S("Contacts") .. [[] + button[6,7.6;2.5,0.5;maillists;]] .. S("Mail lists") .. [[] + button[6,8.7;2.5,0.5;options;]] .. S("Options") .. [[] + button_exit[6,9.5;2.5,0.5;quit;]] .. S("Close") .. [[] + + tooltip[reply;]] .. S("Reply only to the sender") .. [[] + tooltip[replyall;]] .. S("Reply to all involved people") .. [[] + tooltip[forward;]] .. S("Transfer message to other people") .. [[] + + dropdown[0,8.5;2,0.5;sortfield;]] .. + S("From") .. "," .. S("Subject") .. "," .. S("Date") .. [[;]] .. sortfieldindex .. [[;true] + dropdown[2.0,8.5;2,0.5;sortdirection;]] .. + S("Ascending") .. "," .. S("Descending") .. [[;]] .. sortdirection .. [[;true] + field[4.25,8.95;1.4,0.5;filter;]] .. S("Filter") .. [[:;]] .. filter .. [[] + button[5.14,8.62;0.85,0.5;search;Q] + + checkbox[0,9.1;multipleselection;]] .. S("Allow multiple selection") .. [[;]] .. + tostring(mail.selected_idxs.multipleselection[name]) .. [[] + label[0,9.65;]] .. S("@1 of @2 selected", tostring(#mail.selected_idxs.inbox[name]), tostring(#messages)) .. [[] + button[3.5,9.5;2.5,0.5;selectall;]] .. S("(Un)select all") .. [[] + + tablecolumns[color;text;text] + table[0,0.7;5.75,7.45;inbox;]] .. mail.colors.header .. "," .. S("From") .. "," .. S("Subject") + local formspec = { inbox_formspec } + + mail.message_drafts[name] = nil + + local unread_color_enable = mail.get_setting(name, "unreadcolorenable") + local cc_color_enable = mail.get_setting(name, "cccolorenable") + + if #messages > 0 then + for _, message in ipairs(messages) do + local selected_id = 0 + -- check if message is in selection list and return its id + if mail.selected_idxs.inbox[name] and #mail.selected_idxs.inbox[name] > 0 then + for i, selected_msg in ipairs(mail.selected_idxs.inbox[name]) do + if message.id == selected_msg then + selected_id = i + break + end + end + end + if selected_id > 0 then + if not message.read and unread_color_enable then + if not mail.player_in_list(name, message.to) and cc_color_enable then + formspec[#formspec + 1] = "," .. mail.colors.imp_add_sel + else + formspec[#formspec + 1] = "," .. mail.colors.imp_sel + end + else + if not mail.player_in_list(name, message.to) and cc_color_enable then + formspec[#formspec + 1] = "," .. mail.colors.add_sel + else + formspec[#formspec + 1] = "," .. mail.colors.selected + end + end + else + if not message.read and unread_color_enable then + if not mail.player_in_list(name, message.to) and cc_color_enable then + formspec[#formspec + 1] = "," .. mail.colors.imp_add + else + formspec[#formspec + 1] = "," .. mail.colors.important + end + else + if not mail.player_in_list(name, message.to) and cc_color_enable then + formspec[#formspec + 1] = "," .. mail.colors.additional + else + formspec[#formspec + 1] = "," + end + end + end + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = minetest.formspec_escape(message.from) + formspec[#formspec + 1] = "," + if message.subject ~= "" then + if string.len(message.subject) > 30 then + formspec[#formspec + 1] = minetest.formspec_escape(string.sub(message.subject, 1, 27)) + formspec[#formspec + 1] = "..." + else + formspec[#formspec + 1] = minetest.formspec_escape(message.subject) + end + else + formspec[#formspec + 1] = S("(No subject)") + end + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2.25,4.5;" .. S("No mail") .. "]" + end + + minetest.show_formspec(name, "mail:inbox", table.concat(formspec, "")) +end diff --git a/mail/ui/mail.lua b/mail/ui/mail.lua new file mode 100644 index 0000000..9bf114f --- /dev/null +++ b/mail/ui/mail.lua @@ -0,0 +1,17 @@ +-- helper function for tabbed overview + +function mail.show_mail_menu(playername, sortfield, sortdirection, filter) + local index = mail.selected_idxs.boxtab[playername] or 1 + if not mail.selected_idxs.boxtab[playername] then + mail.selected_idxs.boxtab[playername] = 1 + end + if index == 1 then + mail.show_inbox(playername, sortfield, sortdirection, filter) + elseif index == 2 then + mail.show_outbox(playername, sortfield, sortdirection, filter) + elseif index == 3 then + mail.show_drafts(playername) + elseif index == 4 then + mail.show_trash(playername) + end +end diff --git a/mail/ui/maillists.lua b/mail/ui/maillists.lua new file mode 100644 index 0000000..578f97a --- /dev/null +++ b/mail/ui/maillists.lua @@ -0,0 +1,113 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:maillists" + +local maillists_formspec = "size[8,9;]" .. mail.theme .. [[ + button[6,0.10;2,0.5;new;]] .. S("New") .. [[] + button[6,0.85;2,0.5;edit;]] .. S("Edit") .. [[] + button[6,1.60;2,0.5;delete;]] .. S("Delete") .. [[] + button[6,8.25;2,0.5;back;]] .. S("Back") .. [[] + tablecolumns[color;text;text] + table[0,0;5.75,9;maillists;]] .. mail.colors.header .. "," .. S("Name") .. "," .. S("Note") + +function mail.show_maillists(name) + local formspec = { maillists_formspec } + local maillists = mail.get_maillists(name) + + if maillists[1] then + for _, maillist in ipairs(maillists) do + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "@" .. minetest.formspec_escape(maillist.name) + formspec[#formspec + 1] = "," + if maillist.desc ~= "" then + if string.len(maillist.desc) > 30 then + formspec[#formspec + 1] = minetest.formspec_escape(string.sub(maillist.desc, 1, 27)) + formspec[#formspec + 1] = "..." + else + formspec[#formspec + 1] = minetest.formspec_escape(maillist.desc) + end + else + formspec[#formspec + 1] = S("(No description)") + end + end + if mail.selected_idxs.maillists[name] then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = mail.selected_idxs.maillists[name] + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2.25,4.5;" .. S("No maillist") .. "]" + end + minetest.show_formspec(name, FORMNAME, table.concat(formspec, "")) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local maillists = mail.get_maillists(name) + + if fields.maillists then + local evt = minetest.explode_table_event(fields.maillists) + mail.selected_idxs.maillists[name] = evt.row - 1 + if evt.type == "DCL" and maillists[mail.selected_idxs.maillists[name]] then + local maillist = mail.get_maillist_by_name(name, maillists[mail.selected_idxs.maillists[name]].name) + local players_string = mail.concat_player_list(maillist.players) + mail.show_edit_maillist( + name, + maillists[mail.selected_idxs.maillists[name]].name, + maillists[mail.selected_idxs.maillists[name]].desc, + players_string + ) + end + + elseif fields.new then + mail.selected_idxs.maillists[name] = "#NEW#" + mail.show_edit_maillist(name, "", "", "Player1, Player2, Player3") + + elseif fields.edit and maillists[mail.selected_idxs.maillists[name]] then + local maillist = mail.get_maillist_by_name(name, maillists[mail.selected_idxs.maillists[name]].name) + local players_string = mail.concat_player_list(maillist.players) + mail.show_edit_maillist( + name, + maillists[mail.selected_idxs.maillists[name]].name, + maillists[mail.selected_idxs.maillists[name]].desc, + players_string + ) + + elseif fields.delete then + if maillists[mail.selected_idxs.maillists[name]] then + -- delete the maillist and set the selected to the next in the list, + -- except if it was the last. Then determine the new last + local found = false + local last = nil + for k in mail.pairsByKeys(maillists) do + if found then + mail.selected_idxs.maillists[name] = k + break + elseif k == mail.selected_idxs.maillists[name] then + mail.delete_maillist(name, maillists[mail.selected_idxs.maillists[name]].name) + mail.selected_idxs.maillists[name] = nil + found = true + else + last = k + end + end + if found and not mail.selected_idxs.maillists[name] then + -- was the last in the list, so take the previous (new last) + mail.selected_idxs.maillists[name] = last + end + end + + mail.show_maillists(name) + + elseif fields.back then + mail.show_mail_menu(name) + end + + return true +end) diff --git a/mail/ui/message.lua b/mail/ui/message.lua new file mode 100644 index 0000000..0bb3fcf --- /dev/null +++ b/mail/ui/message.lua @@ -0,0 +1,151 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:message" + +function mail.show_message(name, id) + local message = mail.get_message(name, id) + + local formspec = [[ + size[8,9] + + box[0,0;7,1.9;]] .. mail.colors.highlighted .. [[] + + button[7.25,0.15;0.75,0.5;back;X] + + label[0.2,0.1;]] .. S("From") .. [[: %s] + label[0.2,0.5;]] .. S("To") .. [[: %s] + label[0.2,0.9;]] .. S("CC") .. [[: %s] + label[0.2,1.3;]] .. S("Date") .. [[: %s] + tooltip[0.2,1.3;4.8,0.4;]] .. mail.time_ago(message.time) .. [[] + button[5.1,1;2,1;receivers;]] .. S("Receivers") .. [[] + + label[0,2.1;]] .. S("Subject") .. [[: %s] + textarea[0.25,2.6;8,7.0;;;%s] + + button[0,8.5;2,1;reply;]] .. S("Reply") .. [[] + button[2,8.5;2,1;replyall;]] .. S("Reply all") .. [[] + button[4,8.5;2,1;forward;]] .. S("Forward") .. [[] + button[6,8.5;2,1;delete;]] .. S("Delete") .. [[] + + tooltip[reply;]] .. S("Reply only to the sender") .. [[] + tooltip[replyall;]] .. S("Reply to all involved people") .. [[] + tooltip[forward;]] .. S("Transfer message to other people") .. [[] + ]] .. mail.theme + + local from = minetest.formspec_escape(message.from) or "" + local to = minetest.formspec_escape(message.to) or "" + if string.len(to) > 70 then to = string.sub(to, 1, 67) .. "..." end + local cc = minetest.formspec_escape(message.cc) or "" + if string.len(cc) > 50 then cc = string.sub(cc, 1, 47) .. "..." end + local date = type(message.time) == "number" + and minetest.formspec_escape(os.date(mail.get_setting(name, "date_format"), message.time)) or "" + local subject = minetest.formspec_escape(message.subject) or "" + local body = minetest.formspec_escape(message.body) or "" + formspec = string.format(formspec, from, to, cc, date, subject, body) + + if not message.read and mail.get_setting(name, "auto_marking_read") then + -- mark as read + mail.mark_read(name, id) + end + + minetest.show_formspec(name, FORMNAME, formspec) +end + +function mail.reply(name, message) + if not message then + -- TODO: workaround for https://github.com/mt-mods/mail/issues/84 + minetest.log("error", "[mail] reply called with nil message for player: " .. name) + minetest.log("error", "[mail] current mail-context: " .. dump(mail.selected_idxs)) + return + end + local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body + mail.show_compose(name, message.from, "Re: "..message.subject, replyfooter) +end + +function mail.replyall(name, message) + if not message then + -- TODO: workaround for https://github.com/mt-mods/mail/issues/84 + minetest.log("error", "[mail] replyall called with nil message for player: " .. name) + minetest.log("error", "[mail] current mail-context: " .. dump(mail.selected_idxs)) + return + end + + local replyfooter = "Type your reply here.\n\n--Original message follows--\n" ..message.body + + -- new recipients are the sender plus the original recipients, minus ourselves + local recipients = message.to or "" + if message.from ~= nil then + recipients = message.from .. ", " .. recipients + end + recipients = mail.parse_player_list(recipients) + for k,v in pairs(recipients) do + if v == name then + table.remove(recipients, k) + break + end + end + recipients = mail.concat_player_list(recipients) + + -- new CC is old CC minus ourselves + local cc = mail.parse_player_list(message.cc) + for k,v in pairs(cc) do + if v == name then + table.remove(cc, k) + break + end + end + cc = mail.concat_player_list(cc) + + mail.show_compose(name, recipients, "Re: "..message.subject, replyfooter, cc) +end + +function mail.forward(name, message) + local fwfooter = "Type your message here.\n\n--Original message follows--\n" .. (message.body or "") + mail.show_compose(name, "", "Fw: " .. (message.subject or ""), fwfooter) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local entry = mail.get_storage_entry(name) + + local message = "" + if mail.selected_idxs.inbox[name] and mail.selected_idxs.boxtab[name] == 1 then + message = mail.get_message(name, mail.selected_idxs.inbox[name][#mail.selected_idxs.inbox[name]]) + elseif mail.selected_idxs.outbox[name] and mail.selected_idxs.boxtab[name] == 2 then + message = mail.get_message(name, mail.selected_idxs.outbox[name][#mail.selected_idxs.outbox[name]]) + elseif mail.selected_idxs.trash[name] and mail.selected_idxs.boxtab[name] == 4 then + message = mail.get_message(name, entry.trash[mail.selected_idxs.trash[name]].id) + end + + if fields.back then + mail.show_mail_menu(name) + return true -- don't uselessly set messages + + elseif fields.reply then + mail.reply(name, message) + + elseif fields.replyall then + mail.replyall(name, message) + + elseif fields.forward then + mail.forward(name, message) + + elseif fields.delete then + if mail.get_setting(name, "trash_move_enable") and mail.selected_idxs.boxtab[name] ~= 4 then + mail.trash_mail(name, message.id) + else + mail.delete_mail(name, message.id, true) + end + mail.show_mail_menu(name) + + elseif fields.receivers then + mail.show_receivers(name, message.id) + end + + return true +end) diff --git a/mail/ui/options.lua b/mail/ui/options.lua new file mode 100644 index 0000000..f703794 --- /dev/null +++ b/mail/ui/options.lua @@ -0,0 +1,13 @@ +-- helper function for tabbed options + +function mail.show_options(playername) + local index = mail.selected_idxs.optionstab[playername] or 1 + if not mail.selected_idxs.optionstab[playername] then + mail.selected_idxs.optionstab[playername] = 1 + end + if index == 1 then + mail.show_settings(playername) + elseif index == 2 then + mail.show_about(playername) + end +end diff --git a/mail/ui/outbox.lua b/mail/ui/outbox.lua new file mode 100644 index 0000000..8fe4a6d --- /dev/null +++ b/mail/ui/outbox.lua @@ -0,0 +1,115 @@ +-- translation +local S = minetest.get_translator("mail") + +function mail.show_outbox(name, sortfieldindex, sortdirection, filter) + sortfieldindex = tonumber(sortfieldindex or mail.selected_idxs.sortfield[name]) + or mail.get_setting(name, "defaultsortfield") or 3 + sortdirection = tostring(sortdirection or mail.selected_idxs.sortdirection[name] + or mail.get_setting(name, "defaultsortdirection") or "1") + filter = filter or mail.selected_idxs.filter[name] or "" + mail.selected_idxs.outbox[name] = mail.selected_idxs.outbox[name] or {} + + local entry = mail.get_storage_entry(name) + local sortfield = ({"to","subject","time"})[sortfieldindex] + local messages = mail.sort_messages(entry.outbox, sortfield, sortdirection == "2", filter) + + if mail.selected_idxs.outbox[name] and #mail.selected_idxs.outbox[name] > 0 then + for i, selected_msg in ipairs(mail.selected_idxs.outbox[name]) do + local is_present = false + for _, msg in ipairs(messages) do + if msg.id == selected_msg then + is_present = true + break + end + end + if not is_present then + table.remove(mail.selected_idxs.outbox[name], i) + end + end + end + + local trash_tab = "" + if mail.get_setting(name, "trash_move_enable") then + trash_tab = "," .. S("Trash") + end + + local outbox_formspec = "size[8.5,10;]" .. mail.theme .. [[ + tabheader[0.3,1;boxtab;]] .. + S("Inbox") .. "," .. S("Outbox").. "," .. S("Drafts") .. trash_tab .. [[;2;false;false] + + button[6,0.10;2.5,0.5;new;]] .. S("New") .. [[] + button[6,0.95;2.5,0.5;read;]] .. S("Read") .. [[] + button[6,1.70;2.5,0.5;reply;]] .. S("Reply") .. [[] + button[6,2.45;2.5,0.5;replyall;]] .. S("Reply all") .. [[] + button[6,3.20;2.5,0.5;forward;]] .. S("Forward") .. [[] + button[6,3.95;2.5,0.5;delete;]] .. S("Delete") .. [[] + button[6,6.8;2.5,0.5;contacts;]] .. S("Contacts") .. [[] + button[6,7.6;2.5,0.5;maillists;]] .. S("Mail lists") .. [[] + button[6,8.7;2.5,0.5;options;]] .. S("Options") .. [[] + button_exit[6,9.5;2.5,0.5;quit;]] .. S("Close") .. [[] + + tooltip[reply;]] .. S("Reply only to the sender") .. [[] + tooltip[replyall;]] .. S("Reply to all involved people") .. [[] + tooltip[forward;]] .. S("Transfer message to other people") .. [[] + + dropdown[0,8.5;2,0.5;sortfield;]] .. + S("To") .. "," .. S("Subject") .. "," .. S("Date") .. [[;]] .. sortfieldindex .. [[;true] + dropdown[2.0,8.5;2,0.5;sortdirection;]] .. + S("Ascending") .. "," .. S("Descending") .. [[;]] .. sortdirection .. [[;true] + field[4.25,8.95;1.4,0.5;filter;]] .. S("Filter") .. [[:;]] .. filter .. [[] + button[5.14,8.62;0.85,0.5;search;Q] + + checkbox[0,9.1;multipleselection;]] .. S("Allow multiple selection") .. [[;]] .. + tostring(mail.selected_idxs.multipleselection[name]) .. [[] + label[0,9.65;]] .. S("@1 of @2 selected", tostring(#mail.selected_idxs.outbox[name]), tostring(#messages)) ..[[] + button[3.5,9.5;2.5,0.5;selectall;]] .. S("(Un)select all") .. [[] + + tablecolumns[color;text;text] + table[0,0.7;5.75,7.45;outbox;]] .. mail.colors.header .. "," .. S("To") .. "," .. S("Subject") + local formspec = { outbox_formspec } + + mail.message_drafts[name] = nil + + if #messages > 0 then + for _, message in ipairs(messages) do + local selected_id = 0 + -- check if message is in selection list and return its id + if mail.selected_idxs.outbox[name] and #mail.selected_idxs.outbox[name] > 0 then + for i, selected_msg in ipairs(mail.selected_idxs.outbox[name]) do + if message.id == selected_msg then + selected_id = i + break + end + end + end + if selected_id > 0 then + formspec[#formspec + 1] = "," .. mail.colors.selected + else + formspec[#formspec + 1] = "," + end + formspec[#formspec + 1] = "," + if string.len(message.to) > 20 then + formspec[#formspec + 1] = minetest.formspec_escape(string.sub(message.to, 1, 17)) + formspec[#formspec + 1] = "..." + else + formspec[#formspec + 1] = minetest.formspec_escape(message.to) + end + formspec[#formspec + 1] = "," + if message.subject ~= "" then + if string.len(message.subject) > 30 then + formspec[#formspec + 1] = minetest.formspec_escape(string.sub(message.subject, 1, 27)) + formspec[#formspec + 1] = "..." + else + formspec[#formspec + 1] = minetest.formspec_escape(message.subject) + end + else + formspec[#formspec + 1] = S("(No subject)") + end + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2.25,4.5;" .. S("No mail") .. "]" + end + + minetest.show_formspec(name, "mail:outbox", table.concat(formspec, "")) +end diff --git a/mail/ui/receivers.lua b/mail/ui/receivers.lua new file mode 100644 index 0000000..cb953be --- /dev/null +++ b/mail/ui/receivers.lua @@ -0,0 +1,62 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:receivers" + +function mail.show_receivers(name, id) + local message = mail.get_message(name, id) + + local formspec = [[ + size[8,6] + + box[0,0;7,1.1;]] .. mail.colors.highlighted .. [[] + + button[7.25,0.15;0.75,0.5;back;X] + + label[0.2,0.1;]] .. S("From") .. [[: %s] + label[0.2,0.5;]] .. S("Date") .. [[: %s] + + tablecolumns[color;text] + table[0,1.5;3.8,4.5;to;%s] + + tablecolumns[color;text] + table[4,1.5;3.8,4.5;cc;%s] + ]] .. mail.theme + + local from = minetest.formspec_escape(message.from) or "" + local to = mail.parse_player_list(message.to or "") + local to_str = mail.colors.header .. "," .. S("To") .. ",," + to_str = to_str .. table.concat(to, ",,") + local cc = mail.parse_player_list(message.cc or "") + local cc_str = mail.colors.header .. "," .. S("CC") .. ",," + cc_str = cc_str .. table.concat(cc, ",,") + local date = type(message.time) == "number" + and minetest.formspec_escape(os.date(mail.get_setting(name, "date_format"), message.time)) or "" + formspec = string.format(formspec, from, date, to_str, cc_str) + + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local entry = mail.get_storage_entry(name) + + local message_id = "" + if mail.selected_idxs.inbox[name] and mail.selected_idxs.boxtab[name] == 1 then + message_id = mail.selected_idxs.inbox[name][#mail.selected_idxs.inbox[name]] + elseif mail.selected_idxs.outbox[name] and mail.selected_idxs.boxtab[name] == 2 then + message_id = mail.selected_idxs.outbox[name][#mail.selected_idxs.outbox[name]] + elseif mail.selected_idxs.trash[name] and mail.selected_idxs.boxtab[name] == 4 then + message_id = entry.trash[mail.selected_idxs.trash[name]].id + end + + if fields.back then + mail.show_message(name, message_id) + end + + return true +end) diff --git a/mail/ui/select_contact.lua b/mail/ui/select_contact.lua new file mode 100644 index 0000000..4253bdf --- /dev/null +++ b/mail/ui/select_contact.lua @@ -0,0 +1,118 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:selectcontact" + +local select_contact_formspec = "size[8,9;]" .. mail.theme .. [[ + tablecolumns[color;text;text] + table[0,0;3.5,9;contacts;]] .. mail.colors.header .. "," .. S("Name") .. "," .. S("Note") .. [[%s] + button[3.55,2.00;1.75,0.5;toadd;→ ]] .. S("Add") .. [[] + button[3.55,2.75;1.75,0.5;toremove;← ]] .. S("Remove") .. [[] + button[3.55,6.00;1.75,0.5;ccadd;→ ]] .. S("Add") .. [[] + button[3.55,6.75;1.75,0.5;ccremove;← ]] .. S("Remove") .. [[] + tablecolumns[color;text;text] + table[5.15,0.0;2.75,4.5;to;]] .. mail.colors.header .. "," .. S("To") .. ":," .. S("Note") .. [[%s] + tablecolumns[color;text;text] + table[5.15,4.6;2.75,4.5;cc;]] .. mail.colors.header .. "," .. S("CC") .. ":," .. S("Note") .. [[%s] + button[3.55,8.25;1.75,0.5;back;]] .. S("Back") .. [[] +]] + + +function mail.show_select_contact(name, to, cc) + local formspec = select_contact_formspec + local contacts = mail.compile_contact_list(name, mail.selected_idxs.contacts[name]) + + -- compile lists + if to then + to = mail.compile_contact_list(name, mail.selected_idxs.to[name], to) + else + to = "" + end + if cc then + cc = mail.compile_contact_list(name, mail.selected_idxs.cc[name], cc) + else + cc = "" + end + --[[if bcc then + bcc = table.concat(mail.compile_contact_list(name, mail.selected_idxs.bcc[name], bcc) + else + bcc = "" + end]]-- + formspec = string.format(formspec, contacts, to, cc)--, bcc() + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local name = player:get_player_name() + local contacts = mail.get_contacts(name) + local draft = mail.message_drafts[name] + + -- get indexes for fields with selected rows + -- execute their default button's actions if double clicked + for k,action in pairs({ + contacts = "toadd", + to = "toremove", + cc = "ccremove", + bcc = "bccremove" + }) do + if fields[k] then + local evt = minetest.explode_table_event(fields[k]) + mail.selected_idxs[k][name] = evt.row - 1 + if evt.type == "DCL" and mail.selected_idxs[k][name] then + fields[action] = true + end + return true + end + end + + local update = false + -- add + for _,v in pairs({"to","cc","bcc"}) do + if fields[v.."add"] then + update = true + if mail.selected_idxs.contacts[name] then + for k, contact, i in mail.pairsByKeys(contacts) do + if k == mail.selected_idxs.contacts[name] or i == mail.selected_idxs.contacts[name] then + local list = mail.parse_player_list(draft[v]) + list[#list+1] = contact.name + mail.selected_idxs[v][name] = #list + draft[v] = mail.concat_player_list(list) + break + end + end + end + end + end + -- remove + for _,v in pairs({"to","cc","bcc"}) do + if fields[v.."remove"] then + update = true + if mail.selected_idxs[v][name] then + local list = mail.parse_player_list(draft[v]) + table.remove(list, mail.selected_idxs[v][name]) + if #list < mail.selected_idxs[v][name] then + mail.selected_idxs[v][name] = #list + end + draft[v] = mail.concat_player_list(list) + end + end + end + + if update then + mail.show_select_contact(name, draft.to, draft.cc, draft.bcc) + return true + end + + -- delete old idxs + for _,v in ipairs({"contacts","to","cc","bcc"}) do + mail.selected_idxs[v][name] = nil + end + + mail.show_compose(name, draft.to, draft.subject, draft.body, draft.cc, draft.bcc) + + return true +end) diff --git a/mail/ui/settings.lua b/mail/ui/settings.lua new file mode 100644 index 0000000..b448156 --- /dev/null +++ b/mail/ui/settings.lua @@ -0,0 +1,142 @@ +-- translation +local S = minetest.get_translator("mail") + +local FORMNAME = "mail:settings" + +local date_formats = {"%Y-%m-%d %X", "%d/%m/%y %X", "%A %d %B %Y %X"} + +function mail.show_settings(name) + -- date formats prepare + local dates_now = {} + local previous_date_format = mail.get_setting(name, "date_format") + local date_dropdown_index = 1 + for i, f in pairs(date_formats) do + table.insert(dates_now, os.date(f, os.time())) + if f == previous_date_format then date_dropdown_index = i end + end + local date_dropdown_str = table.concat(dates_now, ",") + + local formspec = [[ + size[10,6;] + tabheader[0.3,1;optionstab;]] .. S("Settings") .. "," .. S("About") .. [[;1;false;false] + button[9.35,0;0.75,0.5;back;X] + + box[0,0.8;3,0.45;]] .. mail.colors.highlighted .. [[] + label[0.2,0.8;]] .. S("Notifications") .. [[] + checkbox[0,1.2;chat_notifications;]] .. S("Chat notifications") .. [[;]] .. + tostring(mail.get_setting(name, "chat_notifications")) .. [[] + checkbox[0,1.6;onjoin_notifications;]] .. S("On join notifications") .. [[;]] .. + tostring(mail.get_setting(name, "onjoin_notifications")) .. [[] + checkbox[0,2.0;hud_notifications;]] .. S("HUD notifications") .. [[;]] .. + tostring(mail.get_setting(name, "hud_notifications")) .. [[] + checkbox[0,2.4;sound_notifications;]] .. S("Sound notifications") .. [[;]] .. + tostring(mail.get_setting(name, "sound_notifications")) .. [[] + + box[5,0.8;3,0.45;]] .. mail.colors.highlighted .. [[] + label[5.2,0.8;]] .. S("Message list") .. [[] + checkbox[5,1.2;unreadcolorenable;]] .. S("Show unread in different color") .. [[;]] .. + tostring(mail.get_setting(name, "unreadcolorenable")) .. [[] + checkbox[5,1.6;cccolorenable;]] .. S("Show CC/BCC in different color") .. [[;]] .. + tostring(mail.get_setting(name, "cccolorenable")) .. [[] + + label[5,2.6;]] .. S("Default sorting fields") .. [[] + dropdown[5.5,3.0;2,0.5;defaultsortfield;]] .. + S("From/To") .. "," .. S("Subject") .. "," .. S("Date") .. [[;]] .. + tostring(mail.get_setting(name, "defaultsortfield")) .. [[;true] + dropdown[7.5,3.0;2,0.5;defaultsortdirection;]] .. + S("Ascending") .. "," .. S("Descending") .. [[;]] .. + tostring(mail.get_setting(name, "defaultsortdirection")) .. [[;true] + + box[0,3.2;3,0.45;]] .. mail.colors.highlighted .. [[] + label[0.2,3.2;]] .. S("Other") .. [[] + checkbox[0,3.6;trash_move_enable;]] .. S("Move deleted messages to trash") .. [[;]] .. + tostring(mail.get_setting(name, "trash_move_enable")) .. [[] + checkbox[0,4.0;auto_marking_read;]] .. S("Automatic marking read") .. [[;]] .. + tostring(mail.get_setting(name, "auto_marking_read")) .. [[] + label[0.31,4.7;]] .. S("Date format:") .. [[] + dropdown[2.7,4.6;4,0.5;date_format;]] .. date_dropdown_str .. [[;]] .. + tostring(date_dropdown_index) .. [[;true] + + tooltip[chat_notifications;]] .. S("Receive a message in the chat when there is a new message") .. [[] + tooltip[onjoin_notifications;]] .. S("Receive a message at login when inbox isn't empty") .. [[] + tooltip[hud_notifications;]] .. S("Show an HUD notification when inbox isn't empty") .. [[] + tooltip[sound_notifications;]] .. S("Play a sound when there is a new message") .. [[] + tooltip[auto_marking_read;]] .. S("Mark a message as read when opened") .. [[] + + button[0,5.5;2.5,0.5;save;]] .. S("Save") .. [[] + button[2.7,5.5;2.5,0.5;reset;]] .. S("Reset") .. [[] + ]] .. mail.theme + + minetest.show_formspec(name, FORMNAME, formspec) +end + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= FORMNAME then + return + end + + local playername = player:get_player_name() + + if fields.back then + mail.show_mail_menu(playername) + return + + elseif fields.optionstab == "1" then + mail.selected_idxs.optionstab[playername] = 1 + + elseif fields.optionstab == "2" then + mail.selected_idxs.optionstab[playername] = 2 + mail.show_about(playername) + return + + elseif fields.chat_notifications then + mail.selected_idxs.chat_notifications[playername] = fields.chat_notifications == "true" + + elseif fields.onjoin_notifications then + mail.selected_idxs.onjoin_notifications[playername] = fields.onjoin_notifications == "true" + + elseif fields.hud_notifications then + mail.selected_idxs.hud_notifications[playername] = fields.hud_notifications == "true" + + elseif fields.sound_notifications then + mail.selected_idxs.sound_notifications[playername] = fields.sound_notifications == "true" + + elseif fields.unreadcolorenable then + mail.selected_idxs.unreadcolorenable[playername] = fields.unreadcolorenable == "true" + + elseif fields.cccolorenable then + mail.selected_idxs.cccolorenable[playername] = fields.cccolorenable == "true" + + elseif fields.trash_move_enable then + mail.selected_idxs.trash_move_enable[playername] = fields.trash_move_enable == "true" + + elseif fields.auto_marking_read then + mail.selected_idxs.auto_marking_read[playername] = fields.auto_marking_read == "true" + + elseif fields.save then + -- checkboxes + mail.set_setting(playername, "chat_notifications", mail.selected_idxs.chat_notifications[playername]) + mail.set_setting(playername, "onjoin_notifications", mail.selected_idxs.onjoin_notifications[playername]) + mail.set_setting(playername, "hud_notifications", mail.selected_idxs.hud_notifications[playername]) + mail.set_setting(playername, "sound_notifications", mail.selected_idxs.sound_notifications[playername]) + mail.set_setting(playername, "unreadcolorenable", mail.selected_idxs.unreadcolorenable[playername]) + mail.set_setting(playername, "cccolorenable", mail.selected_idxs.cccolorenable[playername]) + mail.set_setting(playername, "trash_move_enable", mail.selected_idxs.trash_move_enable[playername]) + mail.set_setting(playername, "auto_marking_read", mail.selected_idxs.auto_marking_read[playername]) + -- dropdowns + local defaultsortfield = fields.defaultsortfield or mail.get_setting(playername, "defaultsortfield") + local defaultsortdirection = fields.defaultsortdirection or mail.get_setting(playername, "defaultsortdirection") + local date_format = date_formats[tonumber(fields.date_format)] or mail.get_setting(playername, "date_format") + mail.set_setting(playername, "defaultsortfield", tonumber(defaultsortfield)) + mail.set_setting(playername, "defaultsortdirection", tonumber(defaultsortdirection)) + mail.set_setting(playername, "date_format", date_format) + -- update visuals + mail.hud_update(playername, mail.get_storage_entry(playername).inbox) + mail.show_settings(playername) + + elseif fields.reset then + mail.reset_settings(playername) + mail.show_settings(playername) + end + return +end) diff --git a/mail/ui/trash.lua b/mail/ui/trash.lua new file mode 100644 index 0000000..7601fba --- /dev/null +++ b/mail/ui/trash.lua @@ -0,0 +1,53 @@ +-- translation +local S = minetest.get_translator("mail") + +local trash_formspec = "size[8.5,10;]" .. mail.theme .. [[ + tabheader[0.3,1;boxtab;]] .. + S("Inbox") .. "," .. S("Outbox").. "," .. S("Drafts") .. "," .. S("Trash") .. [[;4;false;false] + + button[6,0.10;2.5,0.5;new;]] .. S("New") .. [[] + button[6,0.95;2.5,0.5;read;]] .. S("Read") .. [[] + button[6,1.70;2.5,0.5;restore;]] .. S("Restore") .. [[] + button[6,2.45;2.5,0.5;delete;]] .. S("Delete") .. [[] + button[6,3.20;2.5,0.5;empty;]] .. S("Empty") .. [[] + button[6,6.8;2.5,0.5;contacts;]] .. S("Contacts") .. [[] + button[6,7.6;2.5,0.5;maillists;]] .. S("Mail lists") .. [[] + button[6,8.7;2.5,0.5;options;]] .. S("Options") .. [[] + button_exit[6,9.5;2.5,0.5;quit;]] .. S("Close") .. [[] + + tablecolumns[color;text;text] + table[0,0.7;5.75,9.35;trash;]] .. mail.colors.header .. "," .. S("From/To") .. "," .. S("Subject") + + +function mail.show_trash(name) + local formspec = { trash_formspec } + local entry = mail.get_storage_entry(name) + local messages = entry.trash + + if messages[1] then + for _, message in ipairs(messages) do + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = minetest.formspec_escape(message.to) + formspec[#formspec + 1] = "," + if message.subject ~= "" then + if string.len(message.subject) > 30 then + formspec[#formspec + 1] = minetest.formspec_escape(string.sub(message.subject, 1, 27)) + formspec[#formspec + 1] = "..." + else + formspec[#formspec + 1] = minetest.formspec_escape(message.subject) + end + else + formspec[#formspec + 1] = S("(No subject)") + end + end + if mail.selected_idxs.trash[name] then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = tostring(mail.selected_idxs.trash[name] + 1) + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2.25,4.5;" .. S("Trash is empty") .. "]" + end + minetest.show_formspec(name, "mail:trash", table.concat(formspec, "")) +end diff --git a/mail/util/contact.lua b/mail/util/contact.lua new file mode 100644 index 0000000..80792b4 --- /dev/null +++ b/mail/util/contact.lua @@ -0,0 +1,75 @@ +-- translation +local S = minetest.get_translator("mail") + +function mail.compile_contact_list(name, selected, playernames) + -- TODO: refactor this - not just compiles *a* list, but *the* list for the contacts screen (too inflexible) + local formspec = {} + local contacts = mail.get_contacts(name) + + if playernames == nil then + local length = 0 + for k, contact, i, l in mail.pairsByKeys(contacts) do + if i == 1 then length = l end + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = minetest.formspec_escape(contact.name) + formspec[#formspec + 1] = "," + local note = contact.note + -- display an ellipsis if the note spans multiple lines + local idx = string.find(note, '\n') + if idx ~= nil then + note = string.sub(note, 1, idx-1) .. ' ...' + end + formspec[#formspec + 1] = minetest.formspec_escape(note) + if type(selected) == "string" then + if string.lower(selected) == k then + selected = i + end + end + end + if length > 0 then + if selected and type(selected) == "number" then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = tostring(selected + 1) + end + formspec[#formspec + 1] = "]" + else + formspec[#formspec + 1] = "]label[2,4.5;" .. S("No contacts") .. "]" + end + else + if type(playernames) == "string" then + playernames = mail.parse_player_list(playernames) + end + for i,c in ipairs(playernames) do + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = "," + formspec[#formspec + 1] = minetest.formspec_escape(c) + formspec[#formspec + 1] = "," + if contacts[string.lower(c)] == nil then + formspec[#formspec + 1] = "" + else + local note = contacts[string.lower(c)].note + -- display an ellipsis if the note spans multiple lines + local idx = string.find(note, '\n') + if idx ~= nil then + note = string.sub(note, 1, idx-1) .. ' ...' + end + formspec[#formspec + 1] = minetest.formspec_escape(note) + end + if not selected then + if type(selected) == "string" then + if string.lower(selected) == string.lower(c) then + selected = i + end + end + end + end + if #playernames > 0 and selected and type(selected) == "number" then + formspec[#formspec + 1] = ";" + formspec[#formspec + 1] = tostring(selected + 1) + end + formspec[#formspec + 1] = "]" + end + return table.concat(formspec, "") + +end diff --git a/mail/util/normalize.lua b/mail/util/normalize.lua new file mode 100644 index 0000000..b817068 --- /dev/null +++ b/mail/util/normalize.lua @@ -0,0 +1,61 @@ +local has_canonical_name = minetest.get_modpath("canonical_name") + +--[[ +return the field normalized (comma separated, single space) +and add individual player names to recipient list +--]] +function mail.normalize_players_and_add_recipients(field, recipients, undeliverable) + local order = mail.parse_player_list(field) + for _, recipient_name in ipairs(order) do + if not minetest.player_exists(recipient_name) then + undeliverable[recipient_name] = true + else + recipients[recipient_name] = true + end + end + return mail.concat_player_list(order) +end + +function mail.parse_player_list(field) + if not field then + return {} + end + + local separator = ", " + local pattern = "([^" .. separator .. "]+)" + + -- get individual players + local player_set = {} + local order = {} + field:gsub(pattern, function(player_name) + local lower = string.lower(player_name) + if not player_set[lower] then + if has_canonical_name then + player_name = canonical_name.get(player_name) or player_name + end + + player_set[lower] = player_name + order[#order+1] = player_name + end + end) + + return order +end + +function mail.concat_player_list(order) + -- turn list of players back into normalized string + return table.concat(order, ", ") +end + +function mail.player_in_list(name, list) + list = list or {} + if type(list) == "string" then + list = mail.parse_player_list(list) + end + for _, player_name in pairs(list) do + if name == player_name then + return true + end + end + return false +end diff --git a/mail/util/normalize.spec.lua b/mail/util/normalize.spec.lua new file mode 100644 index 0000000..88628ad --- /dev/null +++ b/mail/util/normalize.spec.lua @@ -0,0 +1,12 @@ + +mtt.register("util/normalize_players_and_add_recipients", function(callback) + local recipients = {} + local undeliverable = {} + local to = mail.normalize_players_and_add_recipients("player1,player2", recipients, undeliverable) + + assert(to == "player1, player2") + assert(not next(undeliverable)) + assert(recipients["player1"]) + assert(recipients["player2"]) + callback() +end) \ No newline at end of file diff --git a/mail/util/time_ago.lua b/mail/util/time_ago.lua new file mode 100644 index 0000000..39d912a --- /dev/null +++ b/mail/util/time_ago.lua @@ -0,0 +1,29 @@ +-- translation +local S = minetest.get_translator("mail") + +function mail.time_ago(t) + local elapsed = os.time() - t + local str = "" + + local time_units = { + { S("years"), 31536000 }, + { S("months"), 2592000 }, + { S("weeks"), 604800 }, + { S("days"), 86400 }, + { S("hours"), 3600 }, + { S("minuts"), 60 }, + { S("seconds"), 1 }, + } + + for _, u in ipairs(time_units) do + local n = math.modf(elapsed/u[2]) + if n > 0 then + str = str .. " " .. n .. " " .. u[1] + elapsed = elapsed - n * u[2] + end + end + + str = string.sub(str, 2, -1) + + return S("@1 ago", str) +end diff --git a/mail/util/uuid.lua b/mail/util/uuid.lua new file mode 100644 index 0000000..571aa4a --- /dev/null +++ b/mail/util/uuid.lua @@ -0,0 +1,9 @@ +-- source: https://gist.github.com/jrus/3197011 +local random = math.random +function mail.new_uuid() + local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + return string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) + return string.format('%x', v) + end) +end \ No newline at end of file diff --git a/mail/util/uuid.spec.lua b/mail/util/uuid.spec.lua new file mode 100644 index 0000000..549399e --- /dev/null +++ b/mail/util/uuid.spec.lua @@ -0,0 +1,7 @@ + + +mtt.register("uuid", function(callback) + assert(mail.new_uuid()) + assert(mail.new_uuid() ~= mail.new_uuid()) + callback() +end) \ No newline at end of file