From 68f6e6da9bf023e944b755b2d0de4a8f7d5d704c Mon Sep 17 00:00:00 2001 From: rifleh700 Date: Sat, 22 May 2021 13:49:54 +0400 Subject: [PATCH] main files --- .editorconfig | 5 + .gitignore | 2 + LICENSE | 21 + README.md | 4 + conf/acl.xml | 315 +++++ conf/acl_global.xml | 280 +++++ conf/interiors.xml | 192 ++++ conf/skins.xml | 301 +++++ conf/stats.xml | 30 + conf/upgrades.xml | 195 ++++ conf/weathers.xml | 114 ++ core/client/commands.lua | 8 + core/client/globalsettings.lua | 25 + core/client/gui.lua | 200 ++++ core/client/guiguard.lua | 52 + core/client/main.lua | 83 ++ core/client/messages.lua | 54 + core/client/modules.lua | 47 + core/client/session.lua | 25 + core/client/settings.lua | 59 + core/client/sync.lua | 61 + core/client/time.lua | 25 + core/colors.lua | 19 + core/coroutines.lua | 48 + core/definitions.lua | 17 + core/guard.lua | 50 + core/server/acl.lua | 283 +++++ core/server/commands.lua | 346 ++++++ core/server/db.lua | 44 + core/server/events.lua | 28 + core/server/main.lua | 54 + core/server/messages.lua | 76 ++ core/server/modules.lua | 29 + core/server/penaltylogs.lua | 62 + core/server/personalsettings.lua | 43 + core/server/sessions.lua | 151 +++ core/server/settings.lua | 45 + core/server/sync.lua | 140 +++ core/server/time.lua | 8 + env.lua | 8 + gui/default.lua | 34 + gui/definitions.lua | 8 + gui/fonts/DejaVuSansMono-Bold.ttf | Bin 0 -> 331992 bytes gui/helpers.lua | 257 +++++ gui/images/colorpicker/hsvcursor.png | Bin 0 -> 3254 bytes gui/images/colorpicker/hue.png | Bin 0 -> 2819 bytes gui/images/dialog/error.png | Bin 0 -> 2165 bytes gui/images/dialog/info.png | Bin 0 -> 2225 bytes gui/images/dialog/question.png | Bin 0 -> 2221 bytes gui/images/dialog/warning.png | Bin 0 -> 1884 bytes gui/images/elements/dropdown.png | Bin 0 -> 2844 bytes gui/images/elements/info.png | Bin 0 -> 8950 bytes gui/images/elements/minus.png | Bin 0 -> 2821 bytes gui/images/elements/plus.png | Bin 0 -> 2831 bytes gui/images/elements/search.png | Bin 0 -> 1091 bytes gui/images/util/empty.png | Bin 0 -> 959 bytes gui/images/util/white.png | Bin 0 -> 119 bytes gui/lib/bind.lua | 51 + gui/lib/button.lua | 42 + gui/lib/button_combobox.lua | 78 ++ gui/lib/button_icon.lua | 39 + gui/lib/checkbox.lua | 8 + gui/lib/combobox.lua | 9 + gui/lib/customproperty.lua | 29 + gui/lib/data.lua | 29 + gui/lib/edit.lua | 48 + gui/lib/edit_combobox.lua | 90 ++ gui/lib/edit_icon.lua | 20 + gui/lib/edit_matcher.lua | 45 + gui/lib/edit_parser.lua | 22 + gui/lib/elements/container.lua | 11 + gui/lib/elements/gridlayout.lua | 134 +++ gui/lib/elements/horizontallayout.lua | 102 ++ gui/lib/elements/layout.lua | 26 + gui/lib/elements/separator.lua | 35 + gui/lib/elements/verticallayout.lua | 102 ++ gui/lib/events.lua | 74 ++ gui/lib/focus.lua | 26 + gui/lib/font.lua | 87 ++ gui/lib/general.lua | 515 +++++++++ gui/lib/gridlist.lua | 278 +++++ gui/lib/gridlist_focus.lua | 42 + gui/lib/gridlist_keys.lua | 20 + gui/lib/gridlist_players.lua | 85 ++ gui/lib/gridlist_scroll.lua | 52 + gui/lib/gridlist_search.lua | 130 +++ gui/lib/image.lua | 69 ++ gui/lib/input.lua | 50 + gui/lib/input_matcher.lua | 35 + gui/lib/input_parser.lua | 69 ++ gui/lib/internaldata.lua | 26 + gui/lib/label.lua | 148 +++ gui/lib/memo.lua | 40 + gui/lib/memo_parser.lua | 22 + gui/lib/properties.lua | 119 ++ gui/lib/scrollpane.lua | 45 + gui/lib/status.lua | 44 + gui/lib/temptext.lua | 33 + gui/lib/widgets/colorpicker.lua | 376 ++++++ gui/lib/widgets/context.lua | 145 +++ gui/lib/widgets/imagedialog.lua | 144 +++ gui/lib/widgets/imagelist.lua | 374 ++++++ gui/lib/widgets/imageviewer.lua | 142 +++ gui/lib/widgets/inputdialog.lua | 139 +++ gui/lib/widgets/messagedialog.lua | 184 +++ gui/lib/widgets/tooltip.lua | 123 ++ gui/style.lua | 355 ++++++ helpers/client/dx.lua | 16 + helpers/color.lua | 63 + helpers/fightingstyles.lua | 43 + helpers/file.lua | 83 ++ helpers/gameplay.lua | 105 ++ helpers/glitches.lua | 50 + helpers/hash.lua | 24 + helpers/intlocs.lua | 98 ++ helpers/remote.lua | 34 + helpers/server/acl.lua | 108 ++ helpers/server/db.lua | 15 + helpers/server/gameplay.lua | 83 ++ helpers/server/remote.lua | 31 + helpers/server/resource.lua | 92 ++ helpers/skins.lua | 59 + helpers/stats.lua | 86 ++ helpers/time.lua | 79 ++ helpers/upgrades.lua | 55 + helpers/utils.lua | 41 + helpers/vehicle.lua | 111 ++ helpers/walkingstyles.lua | 76 ++ helpers/weapon.lua | 37 + helpers/weathers.lua | 53 + helpers/wsp.lua | 48 + helpers/xml.lua | 28 + libs/check.lua | 183 +++ libs/coroutine.lua | 17 + libs/enum.lua | 12 + libs/math.lua | 15 + libs/string.lua | 134 +++ libs/table.lua | 107 ++ libs/utils.lua | 24 + meta.xml | 599 ++++++++++ modules/about/about_client.lua | 18 + modules/about/about_gui.lua | 58 + modules/acl/acl_client.lua | 116 ++ modules/acl/acl_commands.lua | 163 +++ modules/acl/acl_gui.lua | 72 ++ modules/acl/acl_gui_expert.lua | 699 +++++++++++ modules/acl/acl_gui_simple.lua | 27 + modules/acl/acl_server.lua | 180 +++ modules/admchat/admchat_client.lua | 104 ++ modules/admchat/admchat_commands.lua | 10 + modules/admchat/admchat_gui.lua | 120 ++ modules/admchat/admchat_server.lua | 93 ++ modules/bans/bans_client.lua | 125 ++ modules/bans/bans_commands.lua | 144 +++ modules/bans/bans_gui.lua | 247 ++++ modules/bans/bans_gui_add.lua | 102 ++ modules/bans/bans_server.lua | 128 +++ .../connectionlogs/connectionlogs_client.lua | 66 ++ modules/connectionlogs/connectionlogs_gui.lua | 132 +++ .../connectionlogs/connectionlogs_server.lua | 101 ++ modules/environment/blur/blur_server.lua | 45 + modules/environment/environment_client.lua | 38 + modules/environment/environment_commands.lua | 280 +++++ modules/environment/environment_gui.lua | 332 ++++++ modules/environment/environment_server.lua | 83 ++ modules/execute/execute_client.lua | 122 ++ modules/execute/execute_commands.lua | 21 + modules/execute/execute_gui.lua | 190 +++ modules/fps/fps_client.lua | 61 + modules/fps/fps_server.lua | 44 + modules/ip2c/flags/AD.png | Bin 0 -> 809 bytes modules/ip2c/flags/AE.png | Bin 0 -> 806 bytes modules/ip2c/flags/AF.png | Bin 0 -> 742 bytes modules/ip2c/flags/AG.png | Bin 0 -> 691 bytes modules/ip2c/flags/AI.png | Bin 0 -> 936 bytes modules/ip2c/flags/AL.png | Bin 0 -> 825 bytes modules/ip2c/flags/AM.png | Bin 0 -> 509 bytes modules/ip2c/flags/AN.png | Bin 0 -> 609 bytes modules/ip2c/flags/AO.png | Bin 0 -> 788 bytes modules/ip2c/flags/AQ.png | Bin 0 -> 892 bytes modules/ip2c/flags/AR.png | Bin 0 -> 622 bytes modules/ip2c/flags/AS.png | Bin 0 -> 1004 bytes modules/ip2c/flags/AT.png | Bin 0 -> 509 bytes modules/ip2c/flags/AU.png | Bin 0 -> 879 bytes modules/ip2c/flags/AW.png | Bin 0 -> 643 bytes modules/ip2c/flags/AX.png | Bin 0 -> 620 bytes modules/ip2c/flags/AZ.png | Bin 0 -> 693 bytes modules/ip2c/flags/BA.png | Bin 0 -> 977 bytes modules/ip2c/flags/BB.png | Bin 0 -> 635 bytes modules/ip2c/flags/BD.png | Bin 0 -> 673 bytes modules/ip2c/flags/BE.png | Bin 0 -> 850 bytes modules/ip2c/flags/BF.png | Bin 0 -> 560 bytes modules/ip2c/flags/BG.png | Bin 0 -> 520 bytes modules/ip2c/flags/BH.png | Bin 0 -> 697 bytes modules/ip2c/flags/BI.png | Bin 0 -> 846 bytes modules/ip2c/flags/BJ.png | Bin 0 -> 812 bytes modules/ip2c/flags/BL.png | Bin 0 -> 1117 bytes modules/ip2c/flags/BM.png | Bin 0 -> 902 bytes modules/ip2c/flags/BN.png | Bin 0 -> 1045 bytes modules/ip2c/flags/BO.png | Bin 0 -> 654 bytes modules/ip2c/flags/BR.png | Bin 0 -> 1004 bytes modules/ip2c/flags/BS.png | Bin 0 -> 809 bytes modules/ip2c/flags/BT.png | Bin 0 -> 958 bytes modules/ip2c/flags/BW.png | Bin 0 -> 509 bytes modules/ip2c/flags/BY.png | Bin 0 -> 843 bytes modules/ip2c/flags/BZ.png | Bin 0 -> 906 bytes modules/ip2c/flags/CA.png | Bin 0 -> 632 bytes modules/ip2c/flags/CC.png | Bin 0 -> 824 bytes modules/ip2c/flags/CD.png | Bin 0 -> 973 bytes modules/ip2c/flags/CF.png | Bin 0 -> 614 bytes modules/ip2c/flags/CG.png | Bin 0 -> 915 bytes modules/ip2c/flags/CH.png | Bin 0 -> 507 bytes modules/ip2c/flags/CI.png | Bin 0 -> 851 bytes modules/ip2c/flags/CK.png | Bin 0 -> 965 bytes modules/ip2c/flags/CL.png | Bin 0 -> 698 bytes modules/ip2c/flags/CM.png | Bin 0 -> 906 bytes modules/ip2c/flags/CN.png | Bin 0 -> 845 bytes modules/ip2c/flags/CO.png | Bin 0 -> 509 bytes modules/ip2c/flags/CR.png | Bin 0 -> 509 bytes modules/ip2c/flags/CU.png | Bin 0 -> 845 bytes modules/ip2c/flags/CV.png | Bin 0 -> 648 bytes modules/ip2c/flags/CW.png | Bin 0 -> 633 bytes modules/ip2c/flags/CX.png | Bin 0 -> 938 bytes modules/ip2c/flags/CY.png | Bin 0 -> 859 bytes modules/ip2c/flags/CZ.png | Bin 0 -> 818 bytes modules/ip2c/flags/DE.png | Bin 0 -> 509 bytes modules/ip2c/flags/DJ.png | Bin 0 -> 834 bytes modules/ip2c/flags/DK.png | Bin 0 -> 575 bytes modules/ip2c/flags/DM.png | Bin 0 -> 946 bytes modules/ip2c/flags/DO.png | Bin 0 -> 840 bytes modules/ip2c/flags/DZ.png | Bin 0 -> 911 bytes modules/ip2c/flags/EC.png | Bin 0 -> 730 bytes modules/ip2c/flags/EE.png | Bin 0 -> 509 bytes modules/ip2c/flags/EG.png | Bin 0 -> 616 bytes modules/ip2c/flags/EH.png | Bin 0 -> 843 bytes modules/ip2c/flags/ER.png | Bin 0 -> 935 bytes modules/ip2c/flags/ES.png | Bin 0 -> 705 bytes modules/ip2c/flags/ET.png | Bin 0 -> 889 bytes modules/ip2c/flags/EU.png | Bin 0 -> 832 bytes modules/ip2c/flags/FI.png | Bin 0 -> 589 bytes modules/ip2c/flags/FJ.png | Bin 0 -> 976 bytes modules/ip2c/flags/FK.png | Bin 0 -> 953 bytes modules/ip2c/flags/FM.png | Bin 0 -> 632 bytes modules/ip2c/flags/FO.png | Bin 0 -> 637 bytes modules/ip2c/flags/FR.png | Bin 0 -> 851 bytes modules/ip2c/flags/GA.png | Bin 0 -> 509 bytes modules/ip2c/flags/GB.png | Bin 0 -> 1132 bytes modules/ip2c/flags/GD.png | Bin 0 -> 732 bytes modules/ip2c/flags/GE.png | Bin 0 -> 559 bytes modules/ip2c/flags/GG.png | Bin 0 -> 618 bytes modules/ip2c/flags/GH.png | Bin 0 -> 560 bytes modules/ip2c/flags/GI.png | Bin 0 -> 754 bytes modules/ip2c/flags/GL.png | Bin 0 -> 640 bytes modules/ip2c/flags/GM.png | Bin 0 -> 509 bytes modules/ip2c/flags/GN.png | Bin 0 -> 851 bytes modules/ip2c/flags/GQ.png | Bin 0 -> 827 bytes modules/ip2c/flags/GR.png | Bin 0 -> 671 bytes modules/ip2c/flags/GS.png | Bin 0 -> 1056 bytes modules/ip2c/flags/GT.png | Bin 0 -> 669 bytes modules/ip2c/flags/GU.png | Bin 0 -> 792 bytes modules/ip2c/flags/GW.png | Bin 0 -> 822 bytes modules/ip2c/flags/GY.png | Bin 0 -> 1001 bytes modules/ip2c/flags/HK.png | Bin 0 -> 963 bytes modules/ip2c/flags/HN.png | Bin 0 -> 559 bytes modules/ip2c/flags/HR.png | Bin 0 -> 767 bytes modules/ip2c/flags/HT.png | Bin 0 -> 622 bytes modules/ip2c/flags/HU.png | Bin 0 -> 509 bytes modules/ip2c/flags/IC.png | Bin 0 -> 949 bytes modules/ip2c/flags/ID.png | Bin 0 -> 509 bytes modules/ip2c/flags/IE.png | Bin 0 -> 851 bytes modules/ip2c/flags/IL.png | Bin 0 -> 637 bytes modules/ip2c/flags/IM.png | Bin 0 -> 872 bytes modules/ip2c/flags/IN.png | Bin 0 -> 613 bytes modules/ip2c/flags/IQ.png | Bin 0 -> 686 bytes modules/ip2c/flags/IR.png | Bin 0 -> 842 bytes modules/ip2c/flags/IS.png | Bin 0 -> 626 bytes modules/ip2c/flags/IT.png | Bin 0 -> 851 bytes modules/ip2c/flags/JE.png | Bin 0 -> 926 bytes modules/ip2c/flags/JM.png | Bin 0 -> 868 bytes modules/ip2c/flags/JO.png | Bin 0 -> 811 bytes modules/ip2c/flags/JP.png | Bin 0 -> 626 bytes modules/ip2c/flags/KE.png | Bin 0 -> 729 bytes modules/ip2c/flags/KG.png | Bin 0 -> 920 bytes modules/ip2c/flags/KH.png | Bin 0 -> 755 bytes modules/ip2c/flags/KI.png | Bin 0 -> 1121 bytes modules/ip2c/flags/KM.png | Bin 0 -> 825 bytes modules/ip2c/flags/KN.png | Bin 0 -> 1071 bytes modules/ip2c/flags/KP.png | Bin 0 -> 684 bytes modules/ip2c/flags/KR.png | Bin 0 -> 947 bytes modules/ip2c/flags/KW.png | Bin 0 -> 769 bytes modules/ip2c/flags/KY.png | Bin 0 -> 927 bytes modules/ip2c/flags/KZ.png | Bin 0 -> 858 bytes modules/ip2c/flags/LA.png | Bin 0 -> 593 bytes modules/ip2c/flags/LB.png | Bin 0 -> 714 bytes modules/ip2c/flags/LC.png | Bin 0 -> 610 bytes modules/ip2c/flags/LI.png | Bin 0 -> 686 bytes modules/ip2c/flags/LK.png | Bin 0 -> 944 bytes modules/ip2c/flags/LR.png | Bin 0 -> 696 bytes modules/ip2c/flags/LS.png | Bin 0 -> 608 bytes modules/ip2c/flags/LT.png | Bin 0 -> 509 bytes modules/ip2c/flags/LU.png | Bin 0 -> 509 bytes modules/ip2c/flags/LV.png | Bin 0 -> 509 bytes modules/ip2c/flags/LY.png | Bin 0 -> 634 bytes modules/ip2c/flags/MA.png | Bin 0 -> 767 bytes modules/ip2c/flags/MC.png | Bin 0 -> 509 bytes modules/ip2c/flags/MD.png | Bin 0 -> 741 bytes modules/ip2c/flags/ME.png | Bin 0 -> 847 bytes modules/ip2c/flags/MF.png | Bin 0 -> 621 bytes modules/ip2c/flags/MG.png | Bin 0 -> 821 bytes modules/ip2c/flags/MH.png | Bin 0 -> 942 bytes modules/ip2c/flags/MK.png | Bin 0 -> 789 bytes modules/ip2c/flags/ML.png | Bin 0 -> 851 bytes modules/ip2c/flags/MM.png | Bin 0 -> 638 bytes modules/ip2c/flags/MN.png | Bin 0 -> 793 bytes modules/ip2c/flags/MO.png | Bin 0 -> 877 bytes modules/ip2c/flags/MP.png | Bin 0 -> 1103 bytes modules/ip2c/flags/MQ.png | Bin 0 -> 889 bytes modules/ip2c/flags/MR.png | Bin 0 -> 679 bytes modules/ip2c/flags/MS.png | Bin 0 -> 928 bytes modules/ip2c/flags/MT.png | Bin 0 -> 848 bytes modules/ip2c/flags/MU.png | Bin 0 -> 509 bytes modules/ip2c/flags/MV.png | Bin 0 -> 599 bytes modules/ip2c/flags/MW.png | Bin 0 -> 649 bytes modules/ip2c/flags/MX.png | Bin 0 -> 966 bytes modules/ip2c/flags/MY.png | Bin 0 -> 782 bytes modules/ip2c/flags/MZ.png | Bin 0 -> 827 bytes modules/ip2c/flags/NA.png | Bin 0 -> 1058 bytes modules/ip2c/flags/NC.png | Bin 0 -> 825 bytes modules/ip2c/flags/NE.png | Bin 0 -> 585 bytes modules/ip2c/flags/NF.png | Bin 0 -> 840 bytes modules/ip2c/flags/NG.png | Bin 0 -> 553 bytes modules/ip2c/flags/NI.png | Bin 0 -> 624 bytes modules/ip2c/flags/NL.png | Bin 0 -> 509 bytes modules/ip2c/flags/NO.png | Bin 0 -> 626 bytes modules/ip2c/flags/NP.png | Bin 0 -> 1134 bytes modules/ip2c/flags/NR.png | Bin 0 -> 588 bytes modules/ip2c/flags/NU.png | Bin 0 -> 801 bytes modules/ip2c/flags/NZ.png | Bin 0 -> 809 bytes modules/ip2c/flags/OM.png | Bin 0 -> 816 bytes modules/ip2c/flags/PA.png | Bin 0 -> 877 bytes modules/ip2c/flags/PE.png | Bin 0 -> 553 bytes modules/ip2c/flags/PF.png | Bin 0 -> 713 bytes modules/ip2c/flags/PG.png | Bin 0 -> 933 bytes modules/ip2c/flags/PH.png | Bin 0 -> 932 bytes modules/ip2c/flags/PK.png | Bin 0 -> 822 bytes modules/ip2c/flags/PL.png | Bin 0 -> 509 bytes modules/ip2c/flags/PN.png | Bin 0 -> 967 bytes modules/ip2c/flags/PR.png | Bin 0 -> 845 bytes modules/ip2c/flags/PS.png | Bin 0 -> 749 bytes modules/ip2c/flags/PT.png | Bin 0 -> 869 bytes modules/ip2c/flags/PW.png | Bin 0 -> 672 bytes modules/ip2c/flags/PY.png | Bin 0 -> 624 bytes modules/ip2c/flags/QA.png | Bin 0 -> 706 bytes modules/ip2c/flags/RO.png | Bin 0 -> 851 bytes modules/ip2c/flags/RS.png | Bin 0 -> 731 bytes modules/ip2c/flags/RU.png | Bin 0 -> 520 bytes modules/ip2c/flags/RW.png | Bin 0 -> 629 bytes modules/ip2c/flags/SA.png | Bin 0 -> 939 bytes modules/ip2c/flags/SB.png | Bin 0 -> 943 bytes modules/ip2c/flags/SC.png | Bin 0 -> 903 bytes modules/ip2c/flags/SD.png | Bin 0 -> 772 bytes modules/ip2c/flags/SE.png | Bin 0 -> 575 bytes modules/ip2c/flags/SG.png | Bin 0 -> 745 bytes modules/ip2c/flags/SH.png | Bin 0 -> 928 bytes modules/ip2c/flags/SI.png | Bin 0 -> 602 bytes modules/ip2c/flags/SK.png | Bin 0 -> 667 bytes modules/ip2c/flags/SL.png | Bin 0 -> 509 bytes modules/ip2c/flags/SM.png | Bin 0 -> 766 bytes modules/ip2c/flags/SN.png | Bin 0 -> 908 bytes modules/ip2c/flags/SO.png | Bin 0 -> 615 bytes modules/ip2c/flags/SR.png | Bin 0 -> 564 bytes modules/ip2c/flags/SS.png | Bin 0 -> 849 bytes modules/ip2c/flags/ST.png | Bin 0 -> 788 bytes modules/ip2c/flags/SV.png | Bin 0 -> 624 bytes modules/ip2c/flags/SY.png | Bin 0 -> 559 bytes modules/ip2c/flags/SZ.png | Bin 0 -> 886 bytes modules/ip2c/flags/TC.png | Bin 0 -> 938 bytes modules/ip2c/flags/TD.png | Bin 0 -> 851 bytes modules/ip2c/flags/TF.png | Bin 0 -> 807 bytes modules/ip2c/flags/TG.png | Bin 0 -> 729 bytes modules/ip2c/flags/TH.png | Bin 0 -> 509 bytes modules/ip2c/flags/TJ.png | Bin 0 -> 713 bytes modules/ip2c/flags/TK.png | Bin 0 -> 762 bytes modules/ip2c/flags/TL.png | Bin 0 -> 914 bytes modules/ip2c/flags/TM.png | Bin 0 -> 862 bytes modules/ip2c/flags/TN.png | Bin 0 -> 925 bytes modules/ip2c/flags/TO.png | Bin 0 -> 682 bytes modules/ip2c/flags/TR.png | Bin 0 -> 718 bytes modules/ip2c/flags/TT.png | Bin 0 -> 973 bytes modules/ip2c/flags/TV.png | Bin 0 -> 1019 bytes modules/ip2c/flags/TW.png | Bin 0 -> 750 bytes modules/ip2c/flags/TZ.png | Bin 0 -> 979 bytes modules/ip2c/flags/UA.png | Bin 0 -> 509 bytes modules/ip2c/flags/UG.png | Bin 0 -> 627 bytes modules/ip2c/flags/US.png | Bin 0 -> 765 bytes modules/ip2c/flags/UY.png | Bin 0 -> 769 bytes modules/ip2c/flags/UZ.png | Bin 0 -> 590 bytes modules/ip2c/flags/VA.png | Bin 0 -> 832 bytes modules/ip2c/flags/VC.png | Bin 0 -> 939 bytes modules/ip2c/flags/VE.png | Bin 0 -> 760 bytes modules/ip2c/flags/VG.png | Bin 0 -> 925 bytes modules/ip2c/flags/VI.png | Bin 0 -> 1059 bytes modules/ip2c/flags/VN.png | Bin 0 -> 615 bytes modules/ip2c/flags/VU.png | Bin 0 -> 868 bytes modules/ip2c/flags/WF.png | Bin 0 -> 770 bytes modules/ip2c/flags/WS.png | Bin 0 -> 771 bytes modules/ip2c/flags/YE.png | Bin 0 -> 506 bytes modules/ip2c/flags/YT.png | Bin 0 -> 969 bytes modules/ip2c/flags/ZA.png | Bin 0 -> 953 bytes modules/ip2c/flags/ZM.png | Bin 0 -> 875 bytes modules/ip2c/flags/ZW.png | Bin 0 -> 869 bytes modules/ip2c/flags/ZZ.png | Bin 0 -> 669 bytes modules/ip2c/flags/_abkhazia.png | Bin 0 -> 759 bytes modules/ip2c/flags/_basque-country.png | Bin 0 -> 738 bytes .../flags/_british-antarctic-territory.png | Bin 0 -> 946 bytes modules/ip2c/flags/_england.png | Bin 0 -> 562 bytes modules/ip2c/flags/_gosquared.png | Bin 0 -> 753 bytes modules/ip2c/flags/_kosovo.png | Bin 0 -> 970 bytes modules/ip2c/flags/_nagorno-karabakh.png | Bin 0 -> 665 bytes modules/ip2c/flags/_northern-cyprus.png | Bin 0 -> 641 bytes modules/ip2c/flags/_scotland.png | Bin 0 -> 825 bytes modules/ip2c/flags/_somaliland.png | Bin 0 -> 782 bytes modules/ip2c/flags/_south-ossetia.png | Bin 0 -> 520 bytes modules/ip2c/flags/_unknown.png | Bin 0 -> 669 bytes modules/ip2c/flags/_wales.png | Bin 0 -> 1032 bytes modules/ip2c/ip2c_client.lua | 11 + modules/ip2c/ip2c_server.lua | 85 ++ modules/mute/mute_server.lua | 370 ++++++ modules/mutes/mutes_client.lua | 125 ++ modules/mutes/mutes_commands.lua | 122 ++ modules/mutes/mutes_gui.lua | 241 ++++ modules/mutes/mutes_gui_add.lua | 95 ++ modules/mutes/mutes_server.lua | 110 ++ modules/penaltylogs/penaltylogs_client.lua | 66 ++ modules/penaltylogs/penaltylogs_gui.lua | 136 +++ modules/penaltylogs/penaltylogs_server.lua | 20 + modules/players/images/map.png | Bin 0 -> 4324391 bytes modules/players/images/me.png | Bin 0 -> 1997 bytes modules/players/images/point.png | Bin 0 -> 1610 bytes modules/players/mapdialog_widget.lua | 288 +++++ modules/players/players_client.lua | 76 ++ modules/players/players_commands.lua | 1021 +++++++++++++++++ modules/players/players_gui.lua | 633 ++++++++++ modules/players/players_gui_ban.lua | 99 ++ modules/players/players_gui_mute.lua | 93 ++ modules/players/players_gui_stats.lua | 170 +++ modules/players/players_gui_upgrades.lua | 252 ++++ modules/players/players_server.lua | 155 +++ modules/players/players_spectator_client.lua | 145 +++ modules/players/warp_client.lua | 36 + modules/register/register_server.lua | 28 + modules/report/report_client.lua | 42 + modules/report/report_gui.lua | 127 ++ modules/report/report_server.lua | 214 ++++ modules/reports/reports_client.lua | 200 ++++ modules/reports/reports_commands.lua | 14 + modules/reports/reports_gui.lua | 272 +++++ modules/reports/reports_server.lua | 44 + modules/resources/resources_client.lua | 99 ++ modules/resources/resources_commands.lua | 71 ++ modules/resources/resources_gui.lua | 466 ++++++++ modules/resources/resources_server.lua | 75 ++ modules/screenshot/screenshot_client.lua | 66 ++ .../screenshotsaver_client.lua | 136 +++ modules/server/server_client.lua | 40 + modules/server/server_commands.lua | 65 ++ modules/server/server_gui.lua | 159 +++ modules/server/server_server.lua | 48 + modules/stafflogs/stafflogs_client.lua | 66 ++ modules/stafflogs/stafflogs_gui.lua | 121 ++ modules/stafflogs/stafflogs_server.lua | 20 + modules/teams/teams_commands.lua | 25 + 472 files changed, 22036 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 conf/acl.xml create mode 100644 conf/acl_global.xml create mode 100644 conf/interiors.xml create mode 100644 conf/skins.xml create mode 100644 conf/stats.xml create mode 100644 conf/upgrades.xml create mode 100644 conf/weathers.xml create mode 100644 core/client/commands.lua create mode 100644 core/client/globalsettings.lua create mode 100644 core/client/gui.lua create mode 100644 core/client/guiguard.lua create mode 100644 core/client/main.lua create mode 100644 core/client/messages.lua create mode 100644 core/client/modules.lua create mode 100644 core/client/session.lua create mode 100644 core/client/settings.lua create mode 100644 core/client/sync.lua create mode 100644 core/client/time.lua create mode 100644 core/colors.lua create mode 100644 core/coroutines.lua create mode 100644 core/definitions.lua create mode 100644 core/guard.lua create mode 100644 core/server/acl.lua create mode 100644 core/server/commands.lua create mode 100644 core/server/db.lua create mode 100644 core/server/events.lua create mode 100644 core/server/main.lua create mode 100644 core/server/messages.lua create mode 100644 core/server/modules.lua create mode 100644 core/server/penaltylogs.lua create mode 100644 core/server/personalsettings.lua create mode 100644 core/server/sessions.lua create mode 100644 core/server/settings.lua create mode 100644 core/server/sync.lua create mode 100644 core/server/time.lua create mode 100644 env.lua create mode 100644 gui/default.lua create mode 100644 gui/definitions.lua create mode 100644 gui/fonts/DejaVuSansMono-Bold.ttf create mode 100644 gui/helpers.lua create mode 100644 gui/images/colorpicker/hsvcursor.png create mode 100644 gui/images/colorpicker/hue.png create mode 100644 gui/images/dialog/error.png create mode 100644 gui/images/dialog/info.png create mode 100644 gui/images/dialog/question.png create mode 100644 gui/images/dialog/warning.png create mode 100644 gui/images/elements/dropdown.png create mode 100644 gui/images/elements/info.png create mode 100644 gui/images/elements/minus.png create mode 100644 gui/images/elements/plus.png create mode 100644 gui/images/elements/search.png create mode 100644 gui/images/util/empty.png create mode 100644 gui/images/util/white.png create mode 100644 gui/lib/bind.lua create mode 100644 gui/lib/button.lua create mode 100644 gui/lib/button_combobox.lua create mode 100644 gui/lib/button_icon.lua create mode 100644 gui/lib/checkbox.lua create mode 100644 gui/lib/combobox.lua create mode 100644 gui/lib/customproperty.lua create mode 100644 gui/lib/data.lua create mode 100644 gui/lib/edit.lua create mode 100644 gui/lib/edit_combobox.lua create mode 100644 gui/lib/edit_icon.lua create mode 100644 gui/lib/edit_matcher.lua create mode 100644 gui/lib/edit_parser.lua create mode 100644 gui/lib/elements/container.lua create mode 100644 gui/lib/elements/gridlayout.lua create mode 100644 gui/lib/elements/horizontallayout.lua create mode 100644 gui/lib/elements/layout.lua create mode 100644 gui/lib/elements/separator.lua create mode 100644 gui/lib/elements/verticallayout.lua create mode 100644 gui/lib/events.lua create mode 100644 gui/lib/focus.lua create mode 100644 gui/lib/font.lua create mode 100644 gui/lib/general.lua create mode 100644 gui/lib/gridlist.lua create mode 100644 gui/lib/gridlist_focus.lua create mode 100644 gui/lib/gridlist_keys.lua create mode 100644 gui/lib/gridlist_players.lua create mode 100644 gui/lib/gridlist_scroll.lua create mode 100644 gui/lib/gridlist_search.lua create mode 100644 gui/lib/image.lua create mode 100644 gui/lib/input.lua create mode 100644 gui/lib/input_matcher.lua create mode 100644 gui/lib/input_parser.lua create mode 100644 gui/lib/internaldata.lua create mode 100644 gui/lib/label.lua create mode 100644 gui/lib/memo.lua create mode 100644 gui/lib/memo_parser.lua create mode 100644 gui/lib/properties.lua create mode 100644 gui/lib/scrollpane.lua create mode 100644 gui/lib/status.lua create mode 100644 gui/lib/temptext.lua create mode 100644 gui/lib/widgets/colorpicker.lua create mode 100644 gui/lib/widgets/context.lua create mode 100644 gui/lib/widgets/imagedialog.lua create mode 100644 gui/lib/widgets/imagelist.lua create mode 100644 gui/lib/widgets/imageviewer.lua create mode 100644 gui/lib/widgets/inputdialog.lua create mode 100644 gui/lib/widgets/messagedialog.lua create mode 100644 gui/lib/widgets/tooltip.lua create mode 100644 gui/style.lua create mode 100644 helpers/client/dx.lua create mode 100644 helpers/color.lua create mode 100644 helpers/fightingstyles.lua create mode 100644 helpers/file.lua create mode 100644 helpers/gameplay.lua create mode 100644 helpers/glitches.lua create mode 100644 helpers/hash.lua create mode 100644 helpers/intlocs.lua create mode 100644 helpers/remote.lua create mode 100644 helpers/server/acl.lua create mode 100644 helpers/server/db.lua create mode 100644 helpers/server/gameplay.lua create mode 100644 helpers/server/remote.lua create mode 100644 helpers/server/resource.lua create mode 100644 helpers/skins.lua create mode 100644 helpers/stats.lua create mode 100644 helpers/time.lua create mode 100644 helpers/upgrades.lua create mode 100644 helpers/utils.lua create mode 100644 helpers/vehicle.lua create mode 100644 helpers/walkingstyles.lua create mode 100644 helpers/weapon.lua create mode 100644 helpers/weathers.lua create mode 100644 helpers/wsp.lua create mode 100644 helpers/xml.lua create mode 100644 libs/check.lua create mode 100644 libs/coroutine.lua create mode 100644 libs/enum.lua create mode 100644 libs/math.lua create mode 100644 libs/string.lua create mode 100644 libs/table.lua create mode 100644 libs/utils.lua create mode 100644 meta.xml create mode 100644 modules/about/about_client.lua create mode 100644 modules/about/about_gui.lua create mode 100644 modules/acl/acl_client.lua create mode 100644 modules/acl/acl_commands.lua create mode 100644 modules/acl/acl_gui.lua create mode 100644 modules/acl/acl_gui_expert.lua create mode 100644 modules/acl/acl_gui_simple.lua create mode 100644 modules/acl/acl_server.lua create mode 100644 modules/admchat/admchat_client.lua create mode 100644 modules/admchat/admchat_commands.lua create mode 100644 modules/admchat/admchat_gui.lua create mode 100644 modules/admchat/admchat_server.lua create mode 100644 modules/bans/bans_client.lua create mode 100644 modules/bans/bans_commands.lua create mode 100644 modules/bans/bans_gui.lua create mode 100644 modules/bans/bans_gui_add.lua create mode 100644 modules/bans/bans_server.lua create mode 100644 modules/connectionlogs/connectionlogs_client.lua create mode 100644 modules/connectionlogs/connectionlogs_gui.lua create mode 100644 modules/connectionlogs/connectionlogs_server.lua create mode 100644 modules/environment/blur/blur_server.lua create mode 100644 modules/environment/environment_client.lua create mode 100644 modules/environment/environment_commands.lua create mode 100644 modules/environment/environment_gui.lua create mode 100644 modules/environment/environment_server.lua create mode 100644 modules/execute/execute_client.lua create mode 100644 modules/execute/execute_commands.lua create mode 100644 modules/execute/execute_gui.lua create mode 100644 modules/fps/fps_client.lua create mode 100644 modules/fps/fps_server.lua create mode 100644 modules/ip2c/flags/AD.png create mode 100644 modules/ip2c/flags/AE.png create mode 100644 modules/ip2c/flags/AF.png create mode 100644 modules/ip2c/flags/AG.png create mode 100644 modules/ip2c/flags/AI.png create mode 100644 modules/ip2c/flags/AL.png create mode 100644 modules/ip2c/flags/AM.png create mode 100644 modules/ip2c/flags/AN.png create mode 100644 modules/ip2c/flags/AO.png create mode 100644 modules/ip2c/flags/AQ.png create mode 100644 modules/ip2c/flags/AR.png create mode 100644 modules/ip2c/flags/AS.png create mode 100644 modules/ip2c/flags/AT.png create mode 100644 modules/ip2c/flags/AU.png create mode 100644 modules/ip2c/flags/AW.png create mode 100644 modules/ip2c/flags/AX.png create mode 100644 modules/ip2c/flags/AZ.png create mode 100644 modules/ip2c/flags/BA.png create mode 100644 modules/ip2c/flags/BB.png create mode 100644 modules/ip2c/flags/BD.png create mode 100644 modules/ip2c/flags/BE.png create mode 100644 modules/ip2c/flags/BF.png create mode 100644 modules/ip2c/flags/BG.png create mode 100644 modules/ip2c/flags/BH.png create mode 100644 modules/ip2c/flags/BI.png create mode 100644 modules/ip2c/flags/BJ.png create mode 100644 modules/ip2c/flags/BL.png create mode 100644 modules/ip2c/flags/BM.png create mode 100644 modules/ip2c/flags/BN.png create mode 100644 modules/ip2c/flags/BO.png create mode 100644 modules/ip2c/flags/BR.png create mode 100644 modules/ip2c/flags/BS.png create mode 100644 modules/ip2c/flags/BT.png create mode 100644 modules/ip2c/flags/BW.png create mode 100644 modules/ip2c/flags/BY.png create mode 100644 modules/ip2c/flags/BZ.png create mode 100644 modules/ip2c/flags/CA.png create mode 100644 modules/ip2c/flags/CC.png create mode 100644 modules/ip2c/flags/CD.png create mode 100644 modules/ip2c/flags/CF.png create mode 100644 modules/ip2c/flags/CG.png create mode 100644 modules/ip2c/flags/CH.png create mode 100644 modules/ip2c/flags/CI.png create mode 100644 modules/ip2c/flags/CK.png create mode 100644 modules/ip2c/flags/CL.png create mode 100644 modules/ip2c/flags/CM.png create mode 100644 modules/ip2c/flags/CN.png create mode 100644 modules/ip2c/flags/CO.png create mode 100644 modules/ip2c/flags/CR.png create mode 100644 modules/ip2c/flags/CU.png create mode 100644 modules/ip2c/flags/CV.png create mode 100644 modules/ip2c/flags/CW.png create mode 100644 modules/ip2c/flags/CX.png create mode 100644 modules/ip2c/flags/CY.png create mode 100644 modules/ip2c/flags/CZ.png create mode 100644 modules/ip2c/flags/DE.png create mode 100644 modules/ip2c/flags/DJ.png create mode 100644 modules/ip2c/flags/DK.png create mode 100644 modules/ip2c/flags/DM.png create mode 100644 modules/ip2c/flags/DO.png create mode 100644 modules/ip2c/flags/DZ.png create mode 100644 modules/ip2c/flags/EC.png create mode 100644 modules/ip2c/flags/EE.png create mode 100644 modules/ip2c/flags/EG.png create mode 100644 modules/ip2c/flags/EH.png create mode 100644 modules/ip2c/flags/ER.png create mode 100644 modules/ip2c/flags/ES.png create mode 100644 modules/ip2c/flags/ET.png create mode 100644 modules/ip2c/flags/EU.png create mode 100644 modules/ip2c/flags/FI.png create mode 100644 modules/ip2c/flags/FJ.png create mode 100644 modules/ip2c/flags/FK.png create mode 100644 modules/ip2c/flags/FM.png create mode 100644 modules/ip2c/flags/FO.png create mode 100644 modules/ip2c/flags/FR.png create mode 100644 modules/ip2c/flags/GA.png create mode 100644 modules/ip2c/flags/GB.png create mode 100644 modules/ip2c/flags/GD.png create mode 100644 modules/ip2c/flags/GE.png create mode 100644 modules/ip2c/flags/GG.png create mode 100644 modules/ip2c/flags/GH.png create mode 100644 modules/ip2c/flags/GI.png create mode 100644 modules/ip2c/flags/GL.png create mode 100644 modules/ip2c/flags/GM.png create mode 100644 modules/ip2c/flags/GN.png create mode 100644 modules/ip2c/flags/GQ.png create mode 100644 modules/ip2c/flags/GR.png create mode 100644 modules/ip2c/flags/GS.png create mode 100644 modules/ip2c/flags/GT.png create mode 100644 modules/ip2c/flags/GU.png create mode 100644 modules/ip2c/flags/GW.png create mode 100644 modules/ip2c/flags/GY.png create mode 100644 modules/ip2c/flags/HK.png create mode 100644 modules/ip2c/flags/HN.png create mode 100644 modules/ip2c/flags/HR.png create mode 100644 modules/ip2c/flags/HT.png create mode 100644 modules/ip2c/flags/HU.png create mode 100644 modules/ip2c/flags/IC.png create mode 100644 modules/ip2c/flags/ID.png create mode 100644 modules/ip2c/flags/IE.png create mode 100644 modules/ip2c/flags/IL.png create mode 100644 modules/ip2c/flags/IM.png create mode 100644 modules/ip2c/flags/IN.png create mode 100644 modules/ip2c/flags/IQ.png create mode 100644 modules/ip2c/flags/IR.png create mode 100644 modules/ip2c/flags/IS.png create mode 100644 modules/ip2c/flags/IT.png create mode 100644 modules/ip2c/flags/JE.png create mode 100644 modules/ip2c/flags/JM.png create mode 100644 modules/ip2c/flags/JO.png create mode 100644 modules/ip2c/flags/JP.png create mode 100644 modules/ip2c/flags/KE.png create mode 100644 modules/ip2c/flags/KG.png create mode 100644 modules/ip2c/flags/KH.png create mode 100644 modules/ip2c/flags/KI.png create mode 100644 modules/ip2c/flags/KM.png create mode 100644 modules/ip2c/flags/KN.png create mode 100644 modules/ip2c/flags/KP.png create mode 100644 modules/ip2c/flags/KR.png create mode 100644 modules/ip2c/flags/KW.png create mode 100644 modules/ip2c/flags/KY.png create mode 100644 modules/ip2c/flags/KZ.png create mode 100644 modules/ip2c/flags/LA.png create mode 100644 modules/ip2c/flags/LB.png create mode 100644 modules/ip2c/flags/LC.png create mode 100644 modules/ip2c/flags/LI.png create mode 100644 modules/ip2c/flags/LK.png create mode 100644 modules/ip2c/flags/LR.png create mode 100644 modules/ip2c/flags/LS.png create mode 100644 modules/ip2c/flags/LT.png create mode 100644 modules/ip2c/flags/LU.png create mode 100644 modules/ip2c/flags/LV.png create mode 100644 modules/ip2c/flags/LY.png create mode 100644 modules/ip2c/flags/MA.png create mode 100644 modules/ip2c/flags/MC.png create mode 100644 modules/ip2c/flags/MD.png create mode 100644 modules/ip2c/flags/ME.png create mode 100644 modules/ip2c/flags/MF.png create mode 100644 modules/ip2c/flags/MG.png create mode 100644 modules/ip2c/flags/MH.png create mode 100644 modules/ip2c/flags/MK.png create mode 100644 modules/ip2c/flags/ML.png create mode 100644 modules/ip2c/flags/MM.png create mode 100644 modules/ip2c/flags/MN.png create mode 100644 modules/ip2c/flags/MO.png create mode 100644 modules/ip2c/flags/MP.png create mode 100644 modules/ip2c/flags/MQ.png create mode 100644 modules/ip2c/flags/MR.png create mode 100644 modules/ip2c/flags/MS.png create mode 100644 modules/ip2c/flags/MT.png create mode 100644 modules/ip2c/flags/MU.png create mode 100644 modules/ip2c/flags/MV.png create mode 100644 modules/ip2c/flags/MW.png create mode 100644 modules/ip2c/flags/MX.png create mode 100644 modules/ip2c/flags/MY.png create mode 100644 modules/ip2c/flags/MZ.png create mode 100644 modules/ip2c/flags/NA.png create mode 100644 modules/ip2c/flags/NC.png create mode 100644 modules/ip2c/flags/NE.png create mode 100644 modules/ip2c/flags/NF.png create mode 100644 modules/ip2c/flags/NG.png create mode 100644 modules/ip2c/flags/NI.png create mode 100644 modules/ip2c/flags/NL.png create mode 100644 modules/ip2c/flags/NO.png create mode 100644 modules/ip2c/flags/NP.png create mode 100644 modules/ip2c/flags/NR.png create mode 100644 modules/ip2c/flags/NU.png create mode 100644 modules/ip2c/flags/NZ.png create mode 100644 modules/ip2c/flags/OM.png create mode 100644 modules/ip2c/flags/PA.png create mode 100644 modules/ip2c/flags/PE.png create mode 100644 modules/ip2c/flags/PF.png create mode 100644 modules/ip2c/flags/PG.png create mode 100644 modules/ip2c/flags/PH.png create mode 100644 modules/ip2c/flags/PK.png create mode 100644 modules/ip2c/flags/PL.png create mode 100644 modules/ip2c/flags/PN.png create mode 100644 modules/ip2c/flags/PR.png create mode 100644 modules/ip2c/flags/PS.png create mode 100644 modules/ip2c/flags/PT.png create mode 100644 modules/ip2c/flags/PW.png create mode 100644 modules/ip2c/flags/PY.png create mode 100644 modules/ip2c/flags/QA.png create mode 100644 modules/ip2c/flags/RO.png create mode 100644 modules/ip2c/flags/RS.png create mode 100644 modules/ip2c/flags/RU.png create mode 100644 modules/ip2c/flags/RW.png create mode 100644 modules/ip2c/flags/SA.png create mode 100644 modules/ip2c/flags/SB.png create mode 100644 modules/ip2c/flags/SC.png create mode 100644 modules/ip2c/flags/SD.png create mode 100644 modules/ip2c/flags/SE.png create mode 100644 modules/ip2c/flags/SG.png create mode 100644 modules/ip2c/flags/SH.png create mode 100644 modules/ip2c/flags/SI.png create mode 100644 modules/ip2c/flags/SK.png create mode 100644 modules/ip2c/flags/SL.png create mode 100644 modules/ip2c/flags/SM.png create mode 100644 modules/ip2c/flags/SN.png create mode 100644 modules/ip2c/flags/SO.png create mode 100644 modules/ip2c/flags/SR.png create mode 100644 modules/ip2c/flags/SS.png create mode 100644 modules/ip2c/flags/ST.png create mode 100644 modules/ip2c/flags/SV.png create mode 100644 modules/ip2c/flags/SY.png create mode 100644 modules/ip2c/flags/SZ.png create mode 100644 modules/ip2c/flags/TC.png create mode 100644 modules/ip2c/flags/TD.png create mode 100644 modules/ip2c/flags/TF.png create mode 100644 modules/ip2c/flags/TG.png create mode 100644 modules/ip2c/flags/TH.png create mode 100644 modules/ip2c/flags/TJ.png create mode 100644 modules/ip2c/flags/TK.png create mode 100644 modules/ip2c/flags/TL.png create mode 100644 modules/ip2c/flags/TM.png create mode 100644 modules/ip2c/flags/TN.png create mode 100644 modules/ip2c/flags/TO.png create mode 100644 modules/ip2c/flags/TR.png create mode 100644 modules/ip2c/flags/TT.png create mode 100644 modules/ip2c/flags/TV.png create mode 100644 modules/ip2c/flags/TW.png create mode 100644 modules/ip2c/flags/TZ.png create mode 100644 modules/ip2c/flags/UA.png create mode 100644 modules/ip2c/flags/UG.png create mode 100644 modules/ip2c/flags/US.png create mode 100644 modules/ip2c/flags/UY.png create mode 100644 modules/ip2c/flags/UZ.png create mode 100644 modules/ip2c/flags/VA.png create mode 100644 modules/ip2c/flags/VC.png create mode 100644 modules/ip2c/flags/VE.png create mode 100644 modules/ip2c/flags/VG.png create mode 100644 modules/ip2c/flags/VI.png create mode 100644 modules/ip2c/flags/VN.png create mode 100644 modules/ip2c/flags/VU.png create mode 100644 modules/ip2c/flags/WF.png create mode 100644 modules/ip2c/flags/WS.png create mode 100644 modules/ip2c/flags/YE.png create mode 100644 modules/ip2c/flags/YT.png create mode 100644 modules/ip2c/flags/ZA.png create mode 100644 modules/ip2c/flags/ZM.png create mode 100644 modules/ip2c/flags/ZW.png create mode 100644 modules/ip2c/flags/ZZ.png create mode 100644 modules/ip2c/flags/_abkhazia.png create mode 100644 modules/ip2c/flags/_basque-country.png create mode 100644 modules/ip2c/flags/_british-antarctic-territory.png create mode 100644 modules/ip2c/flags/_england.png create mode 100644 modules/ip2c/flags/_gosquared.png create mode 100644 modules/ip2c/flags/_kosovo.png create mode 100644 modules/ip2c/flags/_nagorno-karabakh.png create mode 100644 modules/ip2c/flags/_northern-cyprus.png create mode 100644 modules/ip2c/flags/_scotland.png create mode 100644 modules/ip2c/flags/_somaliland.png create mode 100644 modules/ip2c/flags/_south-ossetia.png create mode 100644 modules/ip2c/flags/_unknown.png create mode 100644 modules/ip2c/flags/_wales.png create mode 100644 modules/ip2c/ip2c_client.lua create mode 100644 modules/ip2c/ip2c_server.lua create mode 100644 modules/mute/mute_server.lua create mode 100644 modules/mutes/mutes_client.lua create mode 100644 modules/mutes/mutes_commands.lua create mode 100644 modules/mutes/mutes_gui.lua create mode 100644 modules/mutes/mutes_gui_add.lua create mode 100644 modules/mutes/mutes_server.lua create mode 100644 modules/penaltylogs/penaltylogs_client.lua create mode 100644 modules/penaltylogs/penaltylogs_gui.lua create mode 100644 modules/penaltylogs/penaltylogs_server.lua create mode 100644 modules/players/images/map.png create mode 100644 modules/players/images/me.png create mode 100644 modules/players/images/point.png create mode 100644 modules/players/mapdialog_widget.lua create mode 100644 modules/players/players_client.lua create mode 100644 modules/players/players_commands.lua create mode 100644 modules/players/players_gui.lua create mode 100644 modules/players/players_gui_ban.lua create mode 100644 modules/players/players_gui_mute.lua create mode 100644 modules/players/players_gui_stats.lua create mode 100644 modules/players/players_gui_upgrades.lua create mode 100644 modules/players/players_server.lua create mode 100644 modules/players/players_spectator_client.lua create mode 100644 modules/players/warp_client.lua create mode 100644 modules/register/register_server.lua create mode 100644 modules/report/report_client.lua create mode 100644 modules/report/report_gui.lua create mode 100644 modules/report/report_server.lua create mode 100644 modules/reports/reports_client.lua create mode 100644 modules/reports/reports_commands.lua create mode 100644 modules/reports/reports_gui.lua create mode 100644 modules/reports/reports_server.lua create mode 100644 modules/resources/resources_client.lua create mode 100644 modules/resources/resources_commands.lua create mode 100644 modules/resources/resources_gui.lua create mode 100644 modules/resources/resources_server.lua create mode 100644 modules/screenshot/screenshot_client.lua create mode 100644 modules/screenshotsaver/screenshotsaver_client.lua create mode 100644 modules/server/server_client.lua create mode 100644 modules/server/server_commands.lua create mode 100644 modules/server/server_gui.lua create mode 100644 modules/server/server_server.lua create mode 100644 modules/stafflogs/stafflogs_client.lua create mode 100644 modules/stafflogs/stafflogs_gui.lua create mode 100644 modules/stafflogs/stafflogs_server.lua create mode 100644 modules/teams/teams_commands.lua diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..edf70ca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*] +indent_style = tab +indent_size = 4 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a12fc0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +main.db +/data/* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8a7b8e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 rifleh700 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8894c0 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# gradmin +Alternative admin panel for MTA:SA + +Attention! Not a release version. Work in progress. diff --git a/conf/acl.xml b/conf/acl.xml new file mode 100644 index 0000000..7366064 --- /dev/null +++ b/conf/acl.xml @@ -0,0 +1,315 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/acl_global.xml b/conf/acl_global.xml new file mode 100644 index 0000000..f5b6fb3 --- /dev/null +++ b/conf/acl_global.xml @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/interiors.xml b/conf/interiors.xml new file mode 100644 index 0000000..fa85328 --- /dev/null +++ b/conf/interiors.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/skins.xml b/conf/skins.xml new file mode 100644 index 0000000..4c4c12d --- /dev/null +++ b/conf/skins.xml @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/stats.xml b/conf/stats.xml new file mode 100644 index 0000000..4a5468f --- /dev/null +++ b/conf/stats.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/upgrades.xml b/conf/upgrades.xml new file mode 100644 index 0000000..05c9c41 --- /dev/null +++ b/conf/upgrades.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/weathers.xml b/conf/weathers.xml new file mode 100644 index 0000000..bde10ec --- /dev/null +++ b/conf/weathers.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/client/commands.lua b/core/client/commands.lua new file mode 100644 index 0000000..e56543a --- /dev/null +++ b/core/client/commands.lua @@ -0,0 +1,8 @@ + +cCommands = {} + +function cCommands.execute(command, ...) + if not scheck("s") then return false end + + return triggerServerEvent("gra.cCommands.execute", localPlayer, command, ...) +end \ No newline at end of file diff --git a/core/client/globalsettings.lua b/core/client/globalsettings.lua new file mode 100644 index 0000000..9db04d6 --- /dev/null +++ b/core/client/globalsettings.lua @@ -0,0 +1,25 @@ + +addEvent("gra.cGlobalSettings.load", true, true) +addEvent("gra.cGlobalSettings.set", true, true) + +local cache = {} + +cGlobalSettings = {} + +addEventHandler("gra.cGlobalSettings.load", localPlayer, + function(settings) + cache = settings + end +) + +addEventHandler("gra.cGlobalSettings.set", localPlayer, + function(setting, value) + cache[setting] = value + end +) + +function cGlobalSettings.get(setting) + if not scheck("s") then return false end + + return cache[setting] +end \ No newline at end of file diff --git a/core/client/gui.lua b/core/client/gui.lua new file mode 100644 index 0000000..40ce26f --- /dev/null +++ b/core/client/gui.lua @@ -0,0 +1,200 @@ + +addEvent("gra.cGUI.onRefresh", false) +addEvent("gra.cGUI.onSwitch", false) +addEvent("gra.cGUI.messageDialog", true) + +local HEADER = "gradmin "..VERSION +local REFRESH_INTERVAL_TICKS = 500 +local SHOW_INFO_INTERVAL = 5 * 10^3 +local DATE_FORMAT = "%d/%m/%Y %A %X" + +local gs = {} +gs.w = 760 +gs.h = 600 +gs.bar = {} +gs.bar.h = 20 + +local gui = {} +local refreshTicks = getTickCount() + +local function onRender() + if getTickCount() < refreshTicks then return end + + triggerEvent("gra.cGUI.onRefresh", this) + refreshTicks = getTickCount() + REFRESH_INTERVAL_TICKS + +end + +local function onRefresh() + + guiSetText(gui.timeLbl, os.date(DATE_FORMAT, cTime.get())) + +end + +local function onSettingClick() + + cSettings.set("silent", guiCheckBoxGetSelected(gui.silentChk)) + cSettings.set("anonymous", guiCheckBoxGetSelected(gui.anonChk)) + +end + +cGUI = {} + +function cGUI.init() + + gui.window = guiCreateWindow(0, 0, gs.w, gs.h, HEADER, false) + + gui.tpnl = guiCreateTabPanel(GS.mrg2, 20 + GS.mrg, gs.w - GS.mrg2*2, gs.h - 20 - gs.bar.h - GS.mrg*2 - GS.mrg2, false, gui.window) + gui.groups = {} + + gui.bar = guiCreateContainer(GS.mrg2*2, gs.h - GS.mrg2 - gs.bar.h, gs.w - GS.mrg2*4, gs.bar.h, false, gui.window) + + gui.infoLbl = guiCreateLabel(0, 0, 0.5, 1, "", true, gui.bar) + guiLabelSetVerticalAlign(gui.infoLbl, "center") + guiSetEnabled(gui.infoLbl, false) + guiLabelSetColor(gui.infoLbl, unpack(GS.clr.yellow)) + + gui.settingsHlo = guiCreateHorizontalLayout(0.5, 0, 0.2, 1, nil, true, gui.bar) + guiHorizontalLayoutSetVerticalAlign(gui.settingsHlo, "center") + guiHorizontalLayoutSetSizeFixed(gui.settingsHlo, true) + gui.silentChk = guiCreateCheckBox(nil, nil, nil, nil, "Silent", cSettings.get("silent") == true, nil, gui.settingsHlo) + guiSetToolTip(gui.silentChk, "Silent mode disables global messages for commands that you execute") + guiCheckBoxAdjustWidth(gui.silentChk) + gui.anonChk = guiCreateCheckBox(nil, nil, nil, nil, "Anonymous", cSettings.get("anonymous") == true, nil, gui.settingsHlo) + guiSetToolTip(gui.anonChk, "Anonymous mode hides your name for commands that you execute") + guiCheckBoxAdjustWidth(gui.silentChk) + + gui.timeLbl = guiCreateLabel(0.7, 0, 0.3, 1, "", true, gui.bar) + guiSetEnabled(gui.timeLbl, false) + guiLabelSetColor(gui.timeLbl, table.unpack(GS.clr.white)) + guiLabelSetVerticalAlign(gui.timeLbl, "center") + guiLabelSetHorizontalAlign(gui.timeLbl, "right") + + guiRebuildLayouts(gui.window) + + -------------------------------------------- + + addEventHandler("onClientGUIRender", gui.window, onRender, false) + + addEventHandler("gra.cGUI.messageDialog", localPlayer, guiShowMessageDialog) + addEventHandler("gra.cGUI.onRefresh", gui.window, onRefresh, false) + + addEventHandler("onClientGUIClick", gui.settingsHlo, onSettingClick) + + return true +end + +function cGUI.term() + + cGUI.hide() + + removeEventHandler("gra.cGUI.messageDialog", localPlayer, guiShowMessageDialog) + removeEventHandler("onClientGUIClick", gui.settingsHlo, onSettingClick) + + destroyElement(gui.window) + + return true +end + +function cGUI.show() + if guiGetVisible(gui.window) then return false end + + guiSetVisible(gui.window, true, true) + showCursor(true) + triggerEvent("gra.cGUI.onSwitch", gui.window, true) + + return true +end + +function cGUI.hide() + if not guiGetVisible(gui.window) then return false end + + for i, element in ipairs(getElementChildren(guiRoot)) do + if not guiGetData(element, "steady") then guiSetVisible(element, false) end + end + showCursor(false) + triggerEvent("gra.cGUI.onSwitch", gui.window, false) + + return true +end + +function cGUI.switch() + + return guiGetVisible(gui.window) and cGUI.hide() or cGUI.show() +end + +function cGUI.setSteady(element, state) + if not scheck("u:element:gui,b") then return false end + if getElementParent(element) ~= guiRoot then return warn("element can not be steady", 2) and false end + + return guiSetData(element, "steady", state or nil) +end + +function cGUI.getVisible() + if not gui.window then return false end + + return guiGetVisible(gui.window) +end + +function cGUI.addTab(name) + if not gui.window then return false end + + return guiCreateTab(name, gui.tpnl) +end + +function cGUI.getTabGroup(group, name) + if not gui.window then return false end + + if gui.groups[group] then return guiGetData(gui.groups[group], "panel") end + + local tab = guiCreateTab(name, gui.tpnl) + local panel = guiCreateTabPanel(0, 0, 1, 1, true, tab) + guiSetRawPosition(panel, GS.mrg2, GS.mrg2, false) + guiSetRawSize(panel, -GS.mrg2*2, -GS.mrg2*2, false) + guiSetData(tab, "panel", panel) + gui.groups[group] = tab + + return panel +end + +function cGUI.setBarText(text) + if not gui.window then return false end + + return guiSetTempText(gui.infoLbl, text or "", SHOW_INFO_INTERVAL) +end + +addEventHandler("onClientResourceStop", resourceRoot, + function() + showCursor(false) + end +) + +addEventHandler("onClientMouseEnter", guiRoot, + function() + if getElementType(source) ~= "gui-label" then return end + if not guiIsTextClipped(source) then return end + + cGUI.setBarText(guiGetText(source)) + end +) + +addEventHandler("onClientMouseLeave", guiRoot, + function() + if getElementType(source) ~= "gui-label" then return end + if guiGetText(source) ~= guiGetText(gui.infoLbl) then return end + + cGUI.setBarText() + end +) + +addEventHandler("onClientGUIDoubleClick", guiRoot, + function() + if getElementType(source) ~= "gui-label" then return end + + local text = guiGetText(source) + if text == "" then return end + + setClipboard(text) + cGUI.setBarText("Text has been copied to the clipboard") + end +) \ No newline at end of file diff --git a/core/client/guiguard.lua b/core/client/guiguard.lua new file mode 100644 index 0000000..89fa79f --- /dev/null +++ b/core/client/guiguard.lua @@ -0,0 +1,52 @@ + +cGuiGuard = {} + +local elementsPermissions = {} + +addEventHandler("onClientElementDestroy", guiRoot, + function() + elementsPermissions[source] = nil + end +) + +function cGuiGuard.setPermission(element, permission) + check("u:element:gui,s") + if elementsPermissions[element] then return warn("permission is already set", 2) and false end + + elementsPermissions[element] = permission + if not cSession.hasPermissionTo(permission) then + _guiSetEnabled(element, false) + end + + return true +end + +local _guiSetEnabled = guiSetEnabled +function guiSetEnabled(element, enabled) + if not scheck("u:element:gui,b") then return false end + + if not elementsPermissions[element] then return _guiSetEnabled(element, enabled) end + + enabled = enabled and cSession.hasPermissionTo(elementsPermissions[element]) + return _guiSetEnabled(element, enabled) +end + +local _guiCreateButton = guiCreateButton +function guiCreateButton(x, y, w, h, text, relative, parent, permission) + local button = _guiCreateButton(x, y, w, h, text, relative, parent) + if not button then return false end + + if permission then cGuiGuard.setPermission(button, permission) end + + return button +end + +local _guiCreateEdit = guiCreateEdit +function guiCreateEdit(x, y, w, h, text, relative, parent, permission) + local edit = _guiCreateEdit(x, y, w, h, text, relative, parent) + if not edit then return false end + + if permission then cGuiGuard.setPermission(edit, permission) end + + return edit +end \ No newline at end of file diff --git a/core/client/main.lua b/core/client/main.lua new file mode 100644 index 0000000..22ce743 --- /dev/null +++ b/core/client/main.lua @@ -0,0 +1,83 @@ + +local SWITCH_KEY = "p" + +addEvent("gra.switch", true) + +cMain = {} + +cMain.initialized = false + +function cMain.init() + + cGUI.init() + cModules.init() + + bindKey(SWITCH_KEY, "down", SWITCH_COMMAND) + + addEventHandler("onClientResourceStop", resourceRoot, cMain.term) + + cMain.initialized = true + + return true +end + +function cMain.term() + + cMain.initialized = false + + removeEventHandler("onClientResourceStop", resourceRoot, cMain.term) + + unbindKey(SWITCH_KEY, "down", SWITCH_COMMAND) + + cModules.term() + cGUI.term() + + return true +end + +function cMain.isInitialized() + + return cMain.initialized +end + +addEventHandler("onClientResourceStart", resourceRoot, + function() + + triggerServerEvent("gra.onPlayerReady", localPlayer) + + end +) + +addEventHandler("gra.switch", localPlayer, + function() + if not cMain.initialized then return end + + cGUI.switch() + + end +) + +addEventHandler("gra.cSession.onUpdate", localPlayer, + function(wasadmin, nowadmin) + if not wasadmin and not nowadmin then return end + + local wasopen = false + if wasadmin then + wasopen = cGUI.getVisible() + end + + if cMain.initialized then cMain.term() end + + if not nowadmin then return cMessages.outputAdmin("your administration rights have been revoked", COLORS.red) end + + cMain.init() + + if wasadmin then + cMessages.outputAdmin("your administration rights have been updated", COLORS.orange) + if wasopen then cGUI.show() end + else + cMessages.outputAdmin("press "..utf8.upper(SWITCH_KEY).." to open gradmin panel", COLORS.green) + end + + end +) \ No newline at end of file diff --git a/core/client/messages.lua b/core/client/messages.lua new file mode 100644 index 0000000..a1acece --- /dev/null +++ b/core/client/messages.lua @@ -0,0 +1,54 @@ + +local ADMIN_PREFIX = "[gradmin] " +local DEBUG_PREFIX = "INFO: GRADMIN: " +local SECURITY_PREFIX = "SECURITY: " + +cMessages = {} + +local function formatMessage(message, color) + + local hexColor = rgb2hexs(table.unpack(color)) + message = hexColor..utf8.gsub(message, "[^%s]*#%x%x%x%x%x%x[^%s]+", "#FFFFFF%1"..hexColor) + + return message +end + +local function formatSecurityMessage(message, ...) + + local data = table.pack(...) + + local i = 0 + message = utf8.gsub(message, "%$v", + function() + i = i + 1 + return data[i] ~= nil and inspect(data[i]) or NIL_TEXT + end + ) + + return message +end + +function cMessages.outputAdmin(message, color) + if not scheck("s,?t") then return false end + + color = color or COLORS.white + + message = ADMIN_PREFIX..formatMessage(message, color) + local r, g, b = table.unpack(color) + + return outputChatBox(message, r, g, b, true) +end + +function cMessages.outputDebug(message, color) + if not scheck("s,?t") then return false end + + return outputDebugString(DEBUG_PREFIX..message, 4, table.unpack(color or COLORS.white)) +end + +function cMessages.outputSecurity(message, ...) + if not scheck("s") then return false end + + message = SECURITY_PREFIX..formatSecurityMessage(message, ...) + cMessages.outputDebug(message, COLORS.purple) + return true +end \ No newline at end of file diff --git a/core/client/modules.lua b/core/client/modules.lua new file mode 100644 index 0000000..afbfc77 --- /dev/null +++ b/core/client/modules.lua @@ -0,0 +1,47 @@ + +local gradminModules = {} +local modulePermissions = {} +local initializedModules = {} + +cModules = {} + +function cModules.register(mdl, permission) + check("t,s") + if type(mdl.init) ~= "function" then return error("module init function not found", 2) end + if type(mdl.term) ~= "function" then return error("module term function not found", 2) end + if table.index(gradminModules, mdl) then return warn("module is already registered", 2) and false end + + table.insert(gradminModules, mdl) + modulePermissions[mdl] = permission + + return true +end + +function cModules.init() + + for i, mdl in ipairs(gradminModules) do + if cSession.hasPermissionTo(modulePermissions[mdl]) then + if initializedModules[mdl] then + warn("module is already initialized", 2) + else + mdl.init() + initializedModules[mdl] = true + end + end + end + + return true +end + +function cModules.term() + + for i = #gradminModules, 1, -1 do + local mdl = gradminModules[i] + if initializedModules[mdl] then + mdl.term() + initializedModules[mdl] = nil + end + end + + return true +end \ No newline at end of file diff --git a/core/client/session.lua b/core/client/session.lua new file mode 100644 index 0000000..550aac4 --- /dev/null +++ b/core/client/session.lua @@ -0,0 +1,25 @@ + +addEvent("gra.cSession.onUpdate", false) +addEvent("gra.cSession.update", true) + +local permissions = {} + +cSession = {} + +addEventHandler("gra.cSession.update", localPlayer, + function(newPermissions) + + local wasadmin = permissions[GENERAL_PERMISSION] + local nowadmin = newPermissions[GENERAL_PERMISSION] + permissions = newPermissions + + triggerEvent("gra.cSession.onUpdate", localPlayer, wasadmin, nowadmin) + + end +) + +function cSession.hasPermissionTo(right) + if not scheck("s") then return false end + + return permissions[right] == true +end \ No newline at end of file diff --git a/core/client/settings.lua b/core/client/settings.lua new file mode 100644 index 0000000..4b3d9a4 --- /dev/null +++ b/core/client/settings.lua @@ -0,0 +1,59 @@ + +local FILE_PATH = DATA_PATH.."settings.xml" + +local file = nil +local cache = {} + +local remoteSettings = { + "silent", + "anonymous" +} + +cSettings = {} + +function cSettings.init() + + file = xmlLoadFile(FILE_PATH) or xmlCreateFile(FILE_PATH, "settings") + xmlSaveFile(file) + + cache = {} + local data = xmlNodeGetData(file) + for i, child in ipairs(data.children) do + cache[child.attributes.name] = fromJSON(child.attributes.value) + end + + for i, setting in ipairs(remoteSettings) do + triggerServerEvent("gra.cPersonalSettings.set", localPlayer, setting, cache[setting]) + end + + return true +end + +addEventHandler("onClientResourceStart", resourceRoot, cSettings.init) + +function cSettings.get(setting) + if not scheck("s") then return false end + + return cache[setting] +end + +function cSettings.set(setting, value) + if not scheck("s,?b|n|s|t") then return false end + + cache[setting] = value + + local node = xmlFindChildByAttribute(file, "setting", "name", setting) + if not node then + node = xmlCreateChild(file, "setting") + xmlNodeSetAttribute(node, "name", setting) + end + + xmlNodeSetAttribute(node, "value", toJSON(value)) + xmlSaveFile(file) + + if table.index(remoteSettings, setting) then + triggerServerEvent("gra.cPersonalSettings.set", localPlayer, setting, value) + end + + return true +end diff --git a/core/client/sync.lua b/core/client/sync.lua new file mode 100644 index 0000000..b2bb410 --- /dev/null +++ b/core/client/sync.lua @@ -0,0 +1,61 @@ + +addEvent("gra.Sync.send", true) + +local handlers = {} +local requested = {} + +cSync = {} + +function cSync.addHandler(key, f) + if not scheck("s,f") then return false end + + if not handlers[key] then handlers[key] = {} end + if table.index(handlers[key], f) then return warn("handler for '"..key.."' is already added", 2) and false end + + table.insert(handlers[key], f) + + return true +end + +function cSync.removeHandler(key, f) + if not scheck("s,f") then return false end + if not (handlers[key] and table.index(handlers[key], f)) then + return warn("handler for '"..key.."' not found", 2) and false + end + + return table.removevalue(handlers[key], f) and true or false +end + +addEventHandler("gra.cGUI.onSwitch", guiRoot, + function(state) + triggerServerEvent("gra.cSync.gui", localPlayer, state) + end +) + +addEventHandler("gra.Sync.send", localPlayer, + function(tag, key, data) + if sourceResource then return warn(eventName..": invalid source resource for '"..tostring(key).."'", 0) and false end + + if not cMain.isInitialized() then return end + if not handlers[key] then return warn(eventName..": handlers for '"..tostring(key).."' not found", 0) and false end + + if tag then requested[tag] = nil end + + for i, f in ipairs(handlers[key]) do + f(table.unpack(data)) + end + + end +) + +function cSync.request(key, ...) + if not scheck("s") then return false end + if not handlers[key] then return warn("handlers for '"..key.."'' not found", 2) and false end + + local tag = crc32(key, ...) + if requested[tag] then return false end + + requested[tag] = true + + return triggerServerEvent("gra.cSync.request", localPlayer, tag, key, table.pack(...)) +end \ No newline at end of file diff --git a/core/client/time.lua b/core/client/time.lua new file mode 100644 index 0000000..c2c79cd --- /dev/null +++ b/core/client/time.lua @@ -0,0 +1,25 @@ + +addEvent("gra.cTime.sync", true, true) + +local syncedServerTimestamp = nil +local syncedClock = nil + +cTime = {} + +addEventHandler("onClientResourceStart", resourceRoot, + function() + triggerServerEvent("gra.cTime.sync", localPlayer) + end +) + +addEventHandler("gra.cTime.sync", localPlayer, + function(timestamp) + syncedServerTimestamp = timestamp + syncedClock = os.clock() + end +) + +function cTime.get() + + return syncedServerTimestamp + math.floor(os.clock() - syncedClock) +end \ No newline at end of file diff --git a/core/colors.lua b/core/colors.lua new file mode 100644 index 0000000..a9258d7 --- /dev/null +++ b/core/colors.lua @@ -0,0 +1,19 @@ + +COLORS = {} + +COLORS.white = {255, 255, 255 } +COLORS.black = {0, 0, 0 } +COLORS.grey = {127, 127, 127 } +COLORS.red = {255, 127, 127 } +COLORS.orange = {255, 127, 63 } +COLORS.yellow = {255, 255, 127 } +COLORS.green = {127, 255, 127 } +COLORS.aqua = {127, 255, 255 } +COLORS.blue = {127, 127, 255 } +COLORS.purple = {255, 127, 255 } + +COLORS.acl = {255, 168, 0 } +COLORS.console = {223, 149, 232 } + +COLORS.warning = {255, 127, 0 } +COLORS.error = {255, 0, 0 } \ No newline at end of file diff --git a/core/coroutines.lua b/core/coroutines.lua new file mode 100644 index 0000000..dc4f12e --- /dev/null +++ b/core/coroutines.lua @@ -0,0 +1,48 @@ + +local coroutedHandlers = {} +local function coroute(f) + + if not coroutedHandlers[f] then + coroutedHandlers[f] = function(...) coroutine.resume(coroutine.create(f), ...) end + end + return coroutedHandlers[f] +end + +local _addEventHandler = addEventHandler +function addEventHandler(event, element, handler, ...) + if not scheck("s,u:element,f") then return false end + + if not _addEventHandler(event, element, coroute(handler), ...) then + return warn("addEventHandler call failed", 2) and false + end + + return true +end + +local _removeEventHandler = removeEventHandler +function removeEventHandler(event, element, handler) + if not scheck("s,u:element,f") then return false end + + return _removeEventHandler(event, element, coroutedHandlers[handler] or handler) +end + +local _addCommandHandler = addCommandHandler +function addCommandHandler(commandName, handler, ...) + if not scheck("s,f") then return false end + + if not _addCommandHandler(commandName, coroute(handler), ...) then + return warn("addCommandHandler call failed", 2) and false + end + + return true +end + +local _removeCommandHandler = removeCommandHandler +function removeCommandHandler(commandName, handler) + if not scheck("s,?f") then return false end + + if not handler then return _removeCommandHandler(commandName) end + + return removeCommandHandler(commandName, coroute(handler)) +end + diff --git a/core/definitions.lua b/core/definitions.lua new file mode 100644 index 0000000..0f14ca6 --- /dev/null +++ b/core/definitions.lua @@ -0,0 +1,17 @@ + +VERSION = "0.1" + +RESOURCE = getThisResource() +RESOURCE_NAME = getResourceName(RESOURCE) + +GENERAL_PERMISSION = "general.adminpanel" +SWITCH_COMMAND = "gradmin" + +DATA_PATH = "@data/" + +NIL_TEXT = "[none]" + +local _iprint = iprint +function iprint(...) + return _iprint("tick:"..getTickCount(), tostring(coroutine.running()), ...) +end \ No newline at end of file diff --git a/core/guard.lua b/core/guard.lua new file mode 100644 index 0000000..4422a48 --- /dev/null +++ b/core/guard.lua @@ -0,0 +1,50 @@ + +local protectedEvents = {} +local protectedHandlers = {} + +local function wrap(handler) + + if not protectedHandlers[handler] then + protectedHandlers[handler] = function(...) + if sourceResource and sourceResource ~= getThisResource() then + return cMessages.outputSecurity("resource $v tried to get access event $v (access denied)", getResourceName(sourceResource), eventName) and false + end + + handler(...) + + end + end + + return protectedHandlers[handler] +end + +local _addEvent = addEvent +function addEvent(event, ...) + if not _addEvent(event, ...) then return false end + + table.insert(protectedEvents, event) + + return true +end + +local _addEventHandler = addEventHandler +function addEventHandler(event, element, handler, ...) + if not scheck("s,u:element,f") then return false end + + handler = table.index(protectedEvents, event) and wrap(handler) or handler + + if not _addEventHandler(event, element, handler, ...) then + return warn("addEventHandler call failed", 2) and false + end + + return true +end + +local _removeEventHandler = removeEventHandler +function removeEventHandler(event, element, handler) + if not scheck("s,u:element,f") then return false end + + handler = table.index(protectedEvents, event) and wrap(handler) or handler + + return _removeEventHandler(event, element, handler) +end diff --git a/core/server/acl.lua b/core/server/acl.lua new file mode 100644 index 0000000..772524e --- /dev/null +++ b/core/server/acl.lua @@ -0,0 +1,283 @@ + +local RESOURCE_ACLS_PATH = "@conf/acl.xml" +local SERVER_ACL_PATH = "@conf/acl_global.xml" + +cAcl = {} + +function cAcl.init() + + return cAcl.install() +end + +-- compare resource 'Default' ACL entry and server 'Default' ACL entry +-- setup only rights, which not found inside server 'Default' ACL entry +-- setup rights only for resource ACL entries, if it found in server +function cAcl.install() + + local fileNode = xmlLoadFile(RESOURCE_ACLS_PATH, true) + if not fileNode then return cMessages.outputDebug("ACL: default admin ACL conf file not found, please reinstall the resource", COLORS.yellow) and false end + + local fileData = xmlNodeGetData(fileNode) + xmlUnloadFile(fileNode) + + -- get gradmin ACLs rights + local adminACLs = {} + local adminDefaultRights = {} + for i, aclData in ipairs(fileData.children) do + adminACLs[aclData.attributes.name] = {} + for i, rightData in ipairs(aclData.children) do + adminACLs[aclData.attributes.name][rightData.attributes.name] = rightData.attributes.access == "true" + if aclData.attributes.name == "Default" then + table.insert(adminDefaultRights, rightData.attributes.name) + end + end + end + + if not adminACLs["Default"] then return cMessages.outputDebug("ACL: couldn't find 'Default' ACL, please reinstall the resource", COLORS.yellow) and false end + + local defaultACL = aclGet("Default") + if not defaultACL then return cMessages.outputDebug("ACL: couldn't find server 'Default' ACL, please use 'restoreacl' command or reinstall server ACL config", COLORS.yellow) and false end + + -- setup only rights, which not found inside server 'Default' ACL entry + local setupRights = {} + for i, right in ipairs(adminDefaultRights) do + if not aclRightExists(defaultACL, right) then + table.insert(setupRights, right) + end + end + if #setupRights == 0 then return true end + + -- setup rights for every founded admin ACL entry + for aclName, aclRights in pairs(adminACLs) do + local acl = aclGet(aclName) + if acl then + local updated = 0 + for i, right in ipairs(setupRights) do + if aclRights[right] ~= nil and not aclRightExists(acl, right) then + aclSetRight(acl, right, aclRights[right]) + updated = updated + 1 + end + end + if updated > 0 then + cMessages.outputDebug("ACL: updated "..updated.." rights in ACL '"..aclName.."'") + end + end + end + + aclSave() + + return true +end + +-- setup all rights, like resource ACL config +-- create resorce ACL entries if it doesn't exist +-- it does not touch other rights and other ACL entries +function cAcl.restoreGradmin() + + local fileNode = xmlLoadFile(RESOURCE_ACLS_PATH, true) + if not fileNode then return cMessages.outputDebug("ACL: default admin ACL conf file not found, please reinstall the resource", COLORS.yellow) and false end + + local fileData = xmlNodeGetData(fileNode) + xmlUnloadFile(fileNode) + + -- get gradmin ACLs rights + local adminACLs = {} + local adminDefaultRights = {} + for i, aclData in ipairs(fileData.children) do + adminACLs[aclData.attributes.name] = {} + for i, rightData in ipairs(aclData.children) do + adminACLs[aclData.attributes.name][rightData.attributes.name] = rightData.attributes.access == "true" + if aclData.attributes.name == "Default" then + table.insert(adminDefaultRights, rightData.attributes.name) + end + end + end + + if not adminACLs["Default"] then return cMessages.outputDebug("ACL: couldn't find 'Default' ACL, please reinstall the resource", COLORS.yellow) and false end + + -- set all rights like resource ACL config + for aclName, aclRights in pairs(adminACLs) do + local acl = aclGet(aclName) or aclCreate(aclName) + for ir, right in ipairs(adminDefaultRights) do + local access = aclGetRight(acl, right) + if not aclRightExists(acl, right) then + access = nil + end + + if access ~= aclRights[right] then + if aclRights[right] == nil then + aclRemoveRight(acl, right) + else + aclSetRight(acl, right, aclRights[right]) + end + end + end + end + + aclSave() + + cMessages.outputDebug("ACL: admin panel rights successfully restored", COLORS.green) + + return true +end + +function cAcl.restoreServer() + + local fileNode = xmlLoadFile(SERVER_ACL_PATH, true) + if not fileNode then return cMessages.outputDebug("ACL: default server ACL conf file not found, please reinstall the resource", COLORS.yellow) and false end + + local fileData = xmlNodeGetData(fileNode) + xmlUnloadFile(fileNode) + + -- get deault server ACL config + local fileACLs = {} + local fileDefaultACL = nil + local fileGroups = {} + for i, childData in ipairs(fileData.children) do + if childData.name == "acl" then + local data = {} + data.name = childData.attributes.name + data.rights = {} + for i, rightData in ipairs(childData.children) do + table.insert(data.rights, { + name = rightData.attributes.name, + access = rightData.attributes.access == "true" + }) + end + table.insert(fileACLs, data) + if data.name == "Default" then + fileDefaultACL = data + end + + elseif childData.name == "group" then + local data = {} + data.name = childData.attributes.name + data.acls = {} + data.objects = {} + for i, groupChildData in ipairs(childData.children) do + if groupChildData.name == "acl" then + table.insert(data.acls, groupChildData.attributes.name) + elseif groupChildData.name == "object" then + table.insert(data.objects, groupChildData.attributes.name) + end + end + table.insert(fileGroups, data) + end + end + + if not fileDefaultACL then return cMessages.outputDebug("ACL: couldn't find 'Default' ACL in default server ACL! Please reinstall the resource", COLORS.yellow) and false end + + -- temporary ACL entry and group for this resource only + local tempAdminGroup = aclGetGroup("temp_Admin") or aclCreateGroup("temp_Admin") + local tempAdminACL = aclGet("temp_Admin") or aclCreate("temp_Admin") + aclSetRight(tempAdminACL, "function.aclReload", true) + aclSetRight(tempAdminACL, "function.aclSave", true) + aclSetRight(tempAdminACL, "function.aclCreate", true) + aclSetRight(tempAdminACL, "function.aclDestroy", true) + aclSetRight(tempAdminACL, "function.aclSetRight", true) + aclSetRight(tempAdminACL, "function.aclRemoveRight", true) + aclSetRight(tempAdminACL, "function.aclCreateGroup", true) + aclSetRight(tempAdminACL, "function.aclDestroyGroup", true) + aclSetRight(tempAdminACL, "function.aclGroupAddACL", true) + aclSetRight(tempAdminACL, "function.aclGroupRemoveACL", true) + aclSetRight(tempAdminACL, "function.aclGroupAddObject", true) + aclSetRight(tempAdminACL, "function.aclGroupRemoveObject", true) + aclGroupAddACL(tempAdminGroup, tempAdminACL) + aclGroupAddObject(tempAdminGroup, "resource."..getResourceName(getThisResource())) + + -- get or create server ACL entry by file entries + -- remove all server ACL entry rights which founded inside file 'Default' ACL entry + -- set server ACL entry rights(which founded inside file ACL entry) like file ACL entry rights + for i, aclData in ipairs(fileACLs) do + local acl = aclGet(aclData.name) or aclCreate(aclData.name) + for ir, rightData in ipairs(fileDefaultACL.rights) do + if rightData.access then + aclSetRight(acl, true) + else + aclRemoveRight(acl, rightData.name) + end + end + for ir, rightData in ipairs(aclData.rights) do + aclSetRight(acl, rightData.name, rightData.access) + end + end + + -- get or create server group by file groups + -- remove from group "user.*" and "resource.*" + -- remove from group all file ACL entries + -- add to group file group ACL entries + -- add to group file group objects + for i, groupData in ipairs(fileGroups) do + local group = aclGetGroup(groupData.name) or aclCreateGroup(groupData.name) + aclGroupRemoveObject(group, "user.*") + aclGroupRemoveObject(group, "resource.*") + + for ia, aclData in ipairs(fileACLs) do + aclGroupRemoveACL(group, aclGet(aclData.name)) + end + for ia, aclName in ipairs(groupData.acls) do + aclGroupAddACL(group, aclGet(aclName)) + end + for iobj, object in ipairs(groupData.objects) do + aclGroupAddObject(group, object) + end + end + + -- destroy temporary group and ACL entry + aclDestroyGroup(tempAdminGroup) + aclDestroy(tempAdminACL) + + aclSave() + + cMessages.outputDebug("ACL: server ACL successfully restored", COLORS.green) + + return true +end + +cCommands.add("restoreadminacl", + function(admin) + + if not cAcl.restoreGradmin() then return false end + + triggerEvent("gra.onAclChange", root) + + return true, "admin panel ACL successfully restored" + end, + "", "restore gradmin resource ACL", COLORS.acl +) + +cCommands.add("restoreacl", + function(admin) + + local s = cAcl.restoreServer() + local g = cAcl.restoreGradmin() + + if s or g then triggerEvent("gra.onAclChange", root) end + if not (s and g) then return false end + + return true, "server global ACL successfully restored" + end, + "", "restore server global ACL", COLORS.acl +) + +cCommands.add("saveacl", + function(admin) + + if not aclSave() then return false end + + return true, "server ACL file successfully saved" + end, + "", "save server ACL file", COLORS.acl +) + +cCommands.addHardCoded("reloadacl", + function(admin) + + if not aclReload() then return false end + + triggerEvent("gra.onAclChange", root) + + return true, "server ACL successfully reloaded" + end, + COLORS.acl +) \ No newline at end of file diff --git a/core/server/commands.lua b/core/server/commands.lua new file mode 100644 index 0000000..fb51c15 --- /dev/null +++ b/core/server/commands.lua @@ -0,0 +1,346 @@ + +local ANONYMOUS_NAME = "Admin" +local PUBLIC_MESSAGE_DEFAULT_COLOR = COLORS.yellow +local FIND_LOGS_LIMIT = 50 + +addEvent("gra.cCommands.execute", true) + +local commandsData = {} +local hcCommandsData = {} + +local function getAdminGroups(element) + + local groups = {} + for i, group in ipairs(getObjectACLGroups(element)) do + groups[i] = aclGroupGetName(group) + end + return groups +end + +local function formatMessage(message, admin, player) + + local anonymous = cPersonalSettings.get(admin, "anonymous") + local adminName = anonymous and ANONYMOUS_NAME or getPlayerName(admin) + local playerName = player and getPlayerName(player) or "player" + + message = message + :gsub("%$player", playerName) + :gsub("%$admin", adminName) + + return message +end + +local function outputConsoleLog(admin, commandName, description) + + local message = string.format( + "COMMANDS: %s: %s BY (%s) %s (Account: '%s' Serial: %s)", + commandName, description, + table.concat(getAdminGroups(admin), ", "), getPlayerName(admin), + getPlayerAccountName(admin) or NIL_TEXT, admin == console and NIL_TEXT or getPlayerSerial(admin) + ) + + return cMessages.outputConsole(message) +end + +local function outputDBLog(admin, commandName, description) + + return cDB.exec( + [[ + INSERT INTO command_logs ( + timestamp, command, + serial, account, name, plain_name, groups, + description + ) VALUES (?,?,?,?,?,?,?,?) + ]], + getRealTime().timestamp, commandName, + admin == console and "0" or getPlayerSerial(admin), + getPlayerAccountName(admin) or nil, + getPlayerName(admin), getPlayerName(admin, true), + toJSON(getAdminGroups(admin)), + description + ) +end + +local argParsers = { + + ["s"] = function(arg) return arg end, + ["n"] = function(arg) return tonumber(arg) end, + ["b"] = function(arg) return arg == "yes" end, + ["nil"] = function(arg) return arg == "none" end, + ["s-"] = function(args) return table.concat(args, " ") end, + ["ts-"] = function(args) return args end, + ["tn-"] = function(args) + for i, arg in ipairs(args) do + args[i] = tonumber(arg) + if not args[i] then return false end + end + return args + end, + + ["se"] = function(arg) return isValidSerial(arg) and utf8.upper(arg) end, + ["ip"] = function(arg) return isValidIP(arg) and arg end, + ["du"] = function(arg) return parseDuration(arg) end, + ["byte"] = function(arg) + arg = tonumber(arg) + if not arg then return false end + return arg >= 0 and arg <= 255 and arg + end, + + ["P"] = function(arg) + local players = getPlayersByPartialName(arg) + if not players then return false end + if #players == 0 then return false end + if #players > 1 then + return false, #players.." players found" + end + return players[1] + end, + ["T"] = function(arg) return getTeamFromName(arg) end, + ["R"] = function(arg) return getResourceFromName(arg) end, + + ["ven"] = function(arg) return getVehicleModelFromPartialName(utf8.gsub(arg, "_", " ")) end, + ["wen"] = function(arg) return getWeaponIDFromPartialName(utf8.gsub(arg, "_", " ")) end, + ["skn"] = function(arg) return getPedModelFromPartialName(utf8.gsub(arg, "_", " ")) end, + ["fin"] = function(arg) return getFightingStyleFromPartialName(utf8.gsub(arg, "_", " ")) end, + ["wan"] = function(arg) return getWalkingStyleFromPartialName(utf8.gsub(arg, "_", " ")) end, + ["stn"] = function(arg) return getPedStatFromPartialName(utf8.gsub(arg, "_", " ")) end, + ["inn"] = function(arg) return getInteriorLocationFromPartialName(utf8.gsub(arg, "_", " ")) end, + + ["wrn"] = function(arg) return getWeatherFromPartialName(utf8.gsub(arg, "_", " ")) end, +} + +local function parseArgs(commandName, ...) + + local args = table.pack(...) + local commandData = commandsData[commandName] + local result = {} + + for i, variants in ipairs(commandData.types) do + local arg = args[i] + if not arg then + if i >= commandData.opt then return result end + return false, string.format("* Syntax: %s %s", commandName, commandData.syntax) + end + + if utf8.match(variants[1], "-$") then + arg = {} + for ia = i, args.n do + table.insert(arg, args[ia]) + end + end + + local parsed, msg = false, nil + local isFalse, isNil = false, false + for iv, variant in ipairs(variants) do + if variant == "b" and arg == "no" then + isFalse = true + parsed = false + elseif variant == "nil" and arg == "none" then + isNil = true + parsed = nil + else + parsed, msg = argParsers[variant](arg) + end + if parsed or isNil or isFalse then break end + end + + if not (parsed or isFalse or isNil) then + msg = string.format("%s: invalid %s (arg #%d)", commandName, table.concat(commandData.names[i], "/")..(msg and " ("..msg..")" or ""), i) + return false, msg + end + result[i] = parsed + end + + return result +end + +local function parseSyntax(pattern) + + local argTypes = {} + local argNames = {} + local optionalPos = 1 + local syntax = "" + + if not pattern then return argTypes, argNames, optionalPos, syntax end + + local args = split(pattern, ",") + optionalPos = #args + 1 + for i, one in ipairs(args) do + if utf8.match(one, "^%[") then + one = utf8.gsub(one, "^%[", "") + args[i] = one + optionalPos = i + syntax = syntax.."[" + end + argTypes[i] = {} + argNames[i] = {} + syntax = syntax.."<" + for iv, variant in ipairs(split(one, "|")) do + local variantType = utf8.match(variant, ":(.+)$") + if not argParsers[variantType] then error(string.format("parse syntax error: %s type not found", variantType), 3) end + + argTypes[i][iv] = variantType + argNames[i][iv] = utf8.match(variant, "^(.+):") + end + syntax = syntax..table.concat(argNames[i], "/")..">" + if i < #args then syntax = syntax.." " end + end + if optionalPos <= #args then + syntax = syntax.."]" + end + + return argTypes, argNames, optionalPos, syntax +end + +local function mainHandler(admin, commandName, args) + + local commandData = commandsData[commandName] or hcCommandsData[commandName] + if not commandData then return warn("command '"..commandName.."' handler not found", 1) and false end + + local result, resultMessage, publicMessage, playerMessage = commandData.handler(admin, table.unpack(args)) + if result == false then return cMessages.outputAdmin(commandName..": "..(resultMessage or "command failed"), admin, COLORS.red) and false end + + local player = args[1] and type(args[1]) == "userdata" and isElement(args[1]) and getElementType(args[1]) == "player" and args[1] or nil + local outputResultMessage = resultMessage and true or false + local logMessage = formatMessage(resultMessage or commandName.." executed successfully", admin, player) + + outputConsoleLog(admin, commandName, logMessage) + outputDBLog(admin, commandName, logMessage) + + if resultMessage then + cMessages.outputAdmin(formatMessage(resultMessage, admin, player), admin, COLORS.green) + end + if publicMessage and not cPersonalSettings.get(admin, "silent") then + cMessages.outputPublic(formatMessage(publicMessage, admin, player), root, commandData.color or PUBLIC_MESSAGE_DEFAULT_COLOR) + end + if playerMessage and player then + cMessages.outputPublic(formatMessage(playerMessage, admin, player), player, commandData.color or PUBLIC_MESSAGE_DEFAULT_COLOR) + end + + return true +end + +local function onServerCommand(admin, commandName, ...) + if not hasObjectPermissionTo(admin, GENERAL_PERMISSION, false) then + return cMessages.outputSecurity("COMMANDS: client $v doesn't have permission $v", admin, GENERAL_PERMISSION) and false + end + if not hasObjectPermissionTo(admin, "command."..commandName, false) then + return cMessages.outputSecurity("COMMANDS: client $v doesn't have permission $v", admin, "command."..commandName) and false + end + if not commandsData[commandName] then return warn("command '"..commandName.."' handler not found", 1) and false end + + local args, msg = parseArgs(commandName, ...) + if not args then return cMessages.outputAdmin(msg, admin, COLORS.red) and false end + + return mainHandler(admin, commandName, args) +end + +local function onClientRequest(commandName, ...) + local client = client + if not client then return cMessages.outputSecurity("COMMANDS: client not found") and false end + if source ~= client then return cMessages.outputSecurity("COMMANDS: source $v element is not equal client $v", source, client) and false end + if not hasObjectPermissionTo(client, GENERAL_PERMISSION, false) then + return cMessages.outputSecurity("COMMANDS: client $v doesn't have permission $v", client, GENERAL_PERMISSION) and false + end + if not hasObjectPermissionTo(client, "command."..commandName, false) then + return cMessages.outputSecurity("COMMANDS: client $v doesn't have permission $v", client, "command."..commandName) and false + end + + return mainHandler(client, commandName, table.pack(...)) +end + +cCommands = {} + +function cCommands.init() + + cDB.exec([[ + CREATE TABLE IF NOT EXISTS command_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp INTEGER NOT NULL, + command TEXT NOT NULL, + serial TEXT NOT NULL, + account TEXT NOT NULL, + name TEXT NOT NULL, + plain_name TEXT NOT NULL, + groups TEXT NOT NULL, + description TEXT + ) + ]]) + + local publicPermissions = getObjectPermissions("user.*", "command") + + for commandName, commandData in pairs(commandsData) do + if table.index(publicPermissions, "command."..commandName) then + cMessages.outputSecurity("COMMANDS: all users have permission to command $v (fix your ACL)", commandName) + end + addCommandHandler(commandName, onServerCommand, true, true) + end + for commandName, commandData in pairs(hcCommandsData) do + if table.index(publicPermissions, "command."..commandName) then + cMessages.outputSecurity("COMMANDS: all users have permission to hardcoded command $v (fix your ACL)", commandName) + end + end + + addEventHandler("gra.cCommands.execute", root, onClientRequest) + + return true +end + +function cCommands.add(commandName, handlerFunction, pattern, description, color, aliases) + if not scheck("s,f,?s[2],?t") then return false end + if commandsData[commandName] then return warn("command '"..commandName.."' handler is already added", 2) and false end + if hcCommandsData[commandName] then return warn("hardcoded command '"..commandName.."' handler is already added", 2) and false end + + local argTypes, argNames, optionalPos, syntax = parseSyntax(pattern) + commandsData[commandName] = { + handler = handlerFunction, + types = argTypes, + names = argNames, + opt = optionalPos, + syntax = syntax, + desc = description, + color = color, + aliases = aliases + } + + return true +end + +-- need for client execute requests (see "gra.cCommands.execute" event) +function cCommands.addHardCoded(commandName, handlerFunction, color) + if not scheck("s,f,?t") then return false end + if hcCommandsData[commandName] then return warn("hardcoded command '"..commandName.."' handler is already added", 2) and false end + if commandsData[commandName] then return warn("command '"..commandName.."' handler is already added", 2) and false end + + hcCommandsData[commandName] = { + handler = handlerFunction, + color = color, + } + + return true +end + +function cCommands.findLogs(filter, lastID) + if not scheck("?t,?n") then return false end + + filter = filter or {} + + local query = cDB.prepare("SELECT * FROM command_logs WHERE id > ?", lastID or 0) + if filter.since then query = query..cDB.prepare(" AND timestamp > ?", filter.since) end + if filter.before then query = query..cDB.prepare(" AND timestamp < ?", filter.before) end + if filter.command then query = query..cDB.prepare(" AND command LIKE ?", "%"..filter.command.."%") end + if filter.serial then query = query..cDB.prepare(" AND serial LIKE ?", "%"..filter.serial.."%") end + if filter.account then query = query..cDB.prepare(" AND account LIKE ?", "%"..filter.account.."%") end + if filter.name then query = query..cDB.prepare(" AND plain_name LIKE ?", "%"..filter.name.."%") end + if filter.group then query = query..cDB.prepare(" AND groups LIKE ?", "%"..filter.group.."%") end + query = query..cDB.prepare(" LIMIT ?", FIND_LOGS_LIMIT) + + local result = cDB.query(query) + if not result then return result end + + for i, row in ipairs(result) do + row.groups = row.groups and fromJSON(row.groups) or nil + end + + return result +end \ No newline at end of file diff --git a/core/server/db.lua b/core/server/db.lua new file mode 100644 index 0000000..4aad0e7 --- /dev/null +++ b/core/server/db.lua @@ -0,0 +1,44 @@ + +local PATH = "main.db" +local OPTIONS = "batch=1;autoreconnect=1;tag=gradmin;multi_statements=1;" +local TIMEOUT = 2 * 10^3 + +local connection = nil + +cDB = {} + +function cDB.init() + + connection = dbConnect("sqlite", PATH, nil, nil, OPTIONS) + if not connection then return error("db initialization failed", 2) end + + dbExec(connection, "VACUUM") + + return true +end + +cDB.init() + +function cDB.prepare(query, ...) + if not scheck("s") then return false end + + return dbPrepareString(connection, query, ...) +end + +function cDB.prepareBatch(query, t) + if not scheck("s,t") then return false end + + return dbPrepareBatchString(connection, query, t) +end + +function cDB.query(query, ...) + if not scheck("s") then return false end + + return dbPoll(dbQuery(connection, query, ...), TIMEOUT) +end + +function cDB.exec(query, ...) + if not scheck("s") then return false end + + return dbExec(connection, query, ...) +end diff --git a/core/server/events.lua b/core/server/events.lua new file mode 100644 index 0000000..0124f6e --- /dev/null +++ b/core/server/events.lua @@ -0,0 +1,28 @@ + +addEvent("gra.onAclCreate", false) +addEvent("gra.onAclDestroy", false) +addEvent("gra.onAclGroupCreate", false) +addEvent("gra.onAclGroupDestroy", false) +addEvent("gra.onAclRightChange", false) +addEvent("gra.onAclRightRemove", false) +addEvent("gra.onAclGroupACLAdd", false) +addEvent("gra.onAclGroupACLRemove", false) +addEvent("gra.onAclGroupObjectAdd", false) +addEvent("gra.onAclGroupObjectRemove", false) +addEvent("gra.onAclChange", false) +addEvent("gra.onAclAnyChange", false) + +local function onAnyChange() + triggerEvent("gra.onAclAnyChange", root) +end +addEventHandler("gra.onAclCreate", root, onAnyChange, false) +addEventHandler("gra.onAclDestroy", root, onAnyChange, false) +addEventHandler("gra.onAclGroupCreate", root, onAnyChange, false) +addEventHandler("gra.onAclGroupDestroy", root, onAnyChange, false) +addEventHandler("gra.onAclRightChange", root, onAnyChange, false) +addEventHandler("gra.onAclRightRemove", root, onAnyChange, false) +addEventHandler("gra.onAclGroupACLAdd", root, onAnyChange, false) +addEventHandler("gra.onAclGroupACLRemove", root, onAnyChange, false) +addEventHandler("gra.onAclGroupObjectAdd", root, onAnyChange, false) +addEventHandler("gra.onAclGroupObjectRemove", root, onAnyChange, false) +addEventHandler("gra.onAclChange", root, onAnyChange, false) \ No newline at end of file diff --git a/core/server/main.lua b/core/server/main.lua new file mode 100644 index 0000000..187acd3 --- /dev/null +++ b/core/server/main.lua @@ -0,0 +1,54 @@ + +addEvent("gra.onInitialize", false) + +cMain = {} + +cMain.initialized = false + +function cMain.init() + + if not cMain.hasPermissions() then return cMessages.outputDebug("resource has no rights, please use command and restart resource: /aclrequest allow "..RESOURCE_NAME.." all", COLORS.yellow) and false end + + if not cAcl.init() then return cMessages.outputDebug("ACL component initializing failed", COLORS.yellow) and false end + if not cCommands.init() then return cMessages.outputDebug("Commands component initializing failed", COLORS.yellow) and false end + if not cSessions.init() then return cModules.outputDebug("Sessions component initializing failed", COLORS.yellow) and false end + if not cModules.init() then return cModules.outputDebug("Modules component initializing failed", COLORS.yellow) and false end + + addCommandHandler(SWITCH_COMMAND, cMain.onCommand, true) + + cMain.initialized = true + + triggerEvent("gra.onInitialize", resourceRoot) + + return true +end + +function cMain.hasPermissions() + + local requests = getResourceACLRequests(RESOURCE) + for i, request in ipairs(requests) do + if not hasObjectPermissionTo("resource."..RESOURCE_NAME, request.name) then return false end + end + return true +end + +function cMain.onCommand(player) + if not hasObjectPermissionTo(player, GENERAL_PERMISSION) then return end + + triggerClientEvent(player, "gra.switch", player) + +end + +function cMain.isInitialized() + + return cMain.initialized +end + +addEventHandler("onResourceStart", resourceRoot, + function() + + if not cMain.init() then cMessages.outputDebug("initializing failed", COLORS.yellow) end + + end, + false +) \ No newline at end of file diff --git a/core/server/messages.lua b/core/server/messages.lua new file mode 100644 index 0000000..57b1698 --- /dev/null +++ b/core/server/messages.lua @@ -0,0 +1,76 @@ + +local DEBUG_PREFIX = "INFO: GRADMIN: " +local CONSOLE_PREFIX = "GRADMIN: " +local PUBLIC_PREFIX = "[Admin] " +local ADMIN_PREFIX = "[gradmin] " +local SECURITY_PREFIX = "SECURITY: " + + +cMessages = {} + +local function formatMessage(message, color) + + local hexColor = rgb2hexs(table.unpack(color)) + message = hexColor..utf8.gsub(message, "[^%s]*#%x%x%x%x%x%x[^%s]+", "#FFFFFF%1"..hexColor) + + return message +end + +local function formatSecurityMessage(message, ...) + + local data = table.pack(...) + + local i = 0 + message = utf8.gsub(message, "%$v", + function() + i = i + 1 + return data[i] ~= nil and inspect(data[i]) or NIL_TEXT + end + ) + + return message +end + +function cMessages.outputPublic(message, element, color) + if not scheck("s,?u:element:player|u:element:root,?t") then return false end + + color = color or COLORS.white + message = formatMessage(PUBLIC_PREFIX..message, color) + + local r, g, b = table.unpack(color) + return outputChatBox(message, element, r, g, b, true) +end + +function cMessages.outputAdmin(message, element, color) + if not scheck("s,u:element:player|u:element:console,?t") then return false end + + message = ADMIN_PREFIX..message + + if element == console then return outputServerLog(stripColorCodes(message)) end + + color = color or COLORS.white + message = formatMessage(message, color) + + local r, g, b = table.unpack(color) + return outputChatBox(message, element, r, g, b, true) +end + +function cMessages.outputConsole(message) + if not scheck("s") then return false end + + return outputServerLog(CONSOLE_PREFIX..message) +end + +function cMessages.outputDebug(message, color) + if not scheck("s,?t") then return false end + + return outputDebugString(DEBUG_PREFIX..message, 4, table.unpack(color or COLORS.white)) +end + +function cMessages.outputSecurity(message, ...) + if not scheck("s") then return false end + + message = SECURITY_PREFIX..formatSecurityMessage(message, ...) + cMessages.outputDebug(message, COLORS.purple) + return true +end \ No newline at end of file diff --git a/core/server/modules.lua b/core/server/modules.lua new file mode 100644 index 0000000..47b30f6 --- /dev/null +++ b/core/server/modules.lua @@ -0,0 +1,29 @@ + +local gradminModules = {} +local initializedModules = {} + +cModules = {} + +function cModules.register(mdl) + check("t") + if type(mdl.init) ~= "function" then return error("module init function not found", 2) end + if table.index(gradminModules, mdl) then return warn("module is already registered", 2) and false end + + table.insert(gradminModules, mdl) + + return true +end + +function cModules.init() + + for i, mdl in ipairs(gradminModules) do + if initializedModules[mdl] then + warn("module is already initialized", 2) + else + mdl.init() + initializedModules[mdl] = true + end + end + + return true +end \ No newline at end of file diff --git a/core/server/penaltylogs.lua b/core/server/penaltylogs.lua new file mode 100644 index 0000000..4a96ea3 --- /dev/null +++ b/core/server/penaltylogs.lua @@ -0,0 +1,62 @@ + +local FIND_LOGS_LIMIT = 50 + +cPenaltyLogs = {} + +function cPenaltyLogs.init() + + cDB.query([[ + CREATE TABLE IF NOT EXISTS penalty_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp INTEGER NOT NULL, + action TEXT NOT NULL, + admin_account TEXT NOT NULL, + serial TEXT NOT NULL, + data TEXT + ) + ]]) + + return true +end + +cPenaltyLogs.init() + +function cPenaltyLogs.output(admin, action, serial, data) + if not scheck("u:element:player|u:element:console|u:element:root,s,s,?t") then return false end + + return cDB.exec( + [[ + INSERT INTO penalty_logs ( + timestamp, action, admin_account, serial, data + ) VALUES (?,?,?,?,?) + ]], + getRealTime().timestamp, + action, + getPlayerAccountName(admin), + serial, + data and toJSON(data) or nil + ) +end + +function cPenaltyLogs.find(filter, lastID) + if not scheck("?t,?n") then return false end + + filter = filter or {} + + local query = cDB.prepare("SELECT * FROM penalty_logs WHERE id > ?", lastID or 0) + if filter.since then query = query..cDB.prepare(" AND timestamp > ?", filter.since) end + if filter.before then query = query..cDB.prepare(" AND timestamp < ?", filter.before) end + if filter.admin then query = query..cDB.prepare(" AND admin_account LIKE ?", "%"..filter.admin.."%") end + if filter.action then query = query..cDB.prepare(" AND action = ?", filter.action) end + if filter.serial then query = query..cDB.prepare(" AND serial LIKE ?", "%"..filter.serial.."%") end + query = query..cDB.prepare(" LIMIT ?", FIND_LOGS_LIMIT) + + local result = cDB.query(query) + if not result then return result end + + for i, row in ipairs(result) do + row.data = row.data and fromJSON(row.data) or nil + end + + return result +end \ No newline at end of file diff --git a/core/server/personalsettings.lua b/core/server/personalsettings.lua new file mode 100644 index 0000000..122a6ba --- /dev/null +++ b/core/server/personalsettings.lua @@ -0,0 +1,43 @@ + +addEvent("gra.cPersonalSettings.set", true, true) + +local playersSettings = {} + +cPersonalSettings = {} + +addEventHandler("onResourceStart", resourceRoot, + function() + for i, player in ipairs(getElementsByType("player")) do + playersSettings[player] = {} + end + end +) + +addEventHandler("onPlayerJoin", root, + function() + playersSettings[source] = {} + end +) + +addEventHandler("onPlayerQuit", root, + function() + playersSettings[source] = nil + end +) + +addEventHandler("gra.cPersonalSettings.set", root, + function(setting, value) + local client = client + if not playersSettings[client] then return warn(eventName..": client '"..getPlayerName(client).."' not found in table", 0) and false end + if not setting then return warn(eventName..": invalid setting '"..tostring(setting).."'", 0) and false end + + playersSettings[client][setting] = value + end +) + +function cPersonalSettings.get(player, setting) + if not scheck("u:element:player|u:element:root|u:element:console,s") then return false end + if not playersSettings[player] then return nil end + + return playersSettings[player][setting] +end diff --git a/core/server/sessions.lua b/core/server/sessions.lua new file mode 100644 index 0000000..fe54c0f --- /dev/null +++ b/core/server/sessions.lua @@ -0,0 +1,151 @@ + +addEvent("gra.onPlayerReady", true) +addEvent("gra.cSessions.onPlayerUpdate", false) + +local readyPlayers = {} +local currentSessions = {} +local sessionsPermissions = {} + +cSessions = {} + +function cSessions.getPermissions(player) + + local permissions = {} + for gi, group in ipairs(getObjectACLGroups(player)) do + for ai, acl in ipairs(aclGroupListACL(group)) do + for irt, rightType in ipairs({"general", "command"}) do + for ri, right in ipairs(aclListRights(acl, rightType)) do + if aclGetRight(acl, right) then permissions[right] = true end + end + end + end + end + return permissions +end + +function cSessions.init() + + addEventHandler("gra.onInitialize", resourceRoot, cSessions.onInitialize) + addEventHandler("gra.onPlayerReady", root, cSessions.onPlayerReady) + + addEventHandler("onPlayerLogin", root, cSessions.onRelogin) + addEventHandler("onPlayerLogout", root, cSessions.onRelogin) + addEventHandler("onPlayerQuit", root, cSessions.onQuit) + + addEventHandler("gra.onAclDestroy", root, cSessions.updateCurrent, false) + addEventHandler("gra.onAclGroupDestroy", root, cSessions.updateCurrent, false) + addEventHandler("gra.onAclGroupACLAdd", root, cSessions.updateAll, false) + addEventHandler("gra.onAclGroupACLRemove", root, cSessions.updateCurrent, false) + addEventHandler("gra.onAclGroupObjectAdd", root, cSessions.updateAll, false) + addEventHandler("gra.onAclGroupObjectRemove", root, cSessions.updateCurrent, false) + addEventHandler("gra.onAclRightChange", root, cSessions.updateAll, false) + addEventHandler("gra.onAclRightRemove", root, cSessions.updateCurrent, false) + addEventHandler("gra.onAclChange", root, cSessions.updateAll, false) + + return true +end + +function cSessions.update(player) + + local oldPermissions = sessionsPermissions[player] or {} + local newPermissions = cSessions.getPermissions(player) + + local wasadmin = oldPermissions[GENERAL_PERMISSION] + local nowadmin = newPermissions[GENERAL_PERMISSION] + + if not wasadmin and not nowadmin then return end + if table.equal(oldPermissions, newPermissions) then return end + + if nowadmin then + currentSessions[player] = true + sessionsPermissions[player] = newPermissions + else + currentSessions[player] = nil + sessionsPermissions[player] = nil + end + + triggerEvent("gra.cSessions.onPlayerUpdate", player, newPermissions) + triggerClientEvent(player, "gra.cSession.update", player, newPermissions) + + return true +end + +function cSessions.updateCurrent() + + for player in pairs(currentSessions) do + cSessions.update(player) + end + + return true +end + +function cSessions.updateAll() + + for player in pairs(readyPlayers) do + cSessions.update(player) + end + + return true +end + +function cSessions.onInitialize() + + cSessions.updateAll() + +end + +function cSessions.onPlayerReady() + local client = client + if not client then return warn(eventName..": client not found", 0) and false end + if source ~= client then return warn(eventName..": source element is not equal client", 0) and false end + + readyPlayers[source] = true + + if not cMain.isInitialized() then return end + + cSessions.update(source) + +end + +function cSessions.onRelogin() + if not readyPlayers[source] then return end + + cSessions.update(source) + +end + +function cSessions.onQuit() + + currentSessions[source] = nil + readyPlayers[source] = nil + sessionsPermissions[source] = nil + +end + +function cSessions.isAdmin(player) + if not scheck("u:element:player") then return false end + + return currentSessions[player] == true +end + +function cSessions.getAdmins(permission) + if not scheck("?s") then return false end + + local admins = {} + for admin in pairs(currentSessions) do + if not permission or permission and sessionsPermissions[admin][permission] then + table.insert(admins, admin) + end + end + + return admins +end + +function cSessions.getReady() + + local players = {} + for i, player in ipairs(getElementsByType("player")) do + if readyPlayers[player] then players[#players + 1] = player end + end + return players +end \ No newline at end of file diff --git a/core/server/settings.lua b/core/server/settings.lua new file mode 100644 index 0000000..1a5db6a --- /dev/null +++ b/core/server/settings.lua @@ -0,0 +1,45 @@ + +local cache = {} + +addEventHandler("onResourceStart", resourceRoot, + function() + + local settings = {} + local settingsData = getResourceSettingsData() + for setting, data in pairs(settingsData) do + settings[setting] = data.current + end + + cache = settings + + end +) + +addEventHandler("gra.onPlayerReady", root, + function() + + triggerClientEvent(source, "gra.cGlobalSettings.load", source, cache) + + end +) + +addEventHandler("onSettingChange", root, + function(setting, oldValue, newValue) + + local resourceName = getResourceNameFromSetting(setting) + if not resourceName then return end + + local res = getResourceFromName(resourceName) + if not res then return end + if res ~= getThisResource() then return end + + local shortSetting = utf8.match(setting, "^.+%.(.+)$") + local validValue = getResourceSetting(nil, shortSetting) + cache[shortSetting] = validValue + + for i, player in ipairs(cSessions.getReady()) do + triggerClientEvent(player, "gra.cGlobalSettings.set", player, shortSetting, validValue) + end + + end +) \ No newline at end of file diff --git a/core/server/sync.lua b/core/server/sync.lua new file mode 100644 index 0000000..e26a978 --- /dev/null +++ b/core/server/sync.lua @@ -0,0 +1,140 @@ + +local DEFAULT_BANDWIDTH = 100 * 10^3 +local LATENT_BANDWIDTH = 50 * 10^3 + +addEvent("gra.cSync.request", true) +addEvent("gra.cSync.gui", true) + +local activeClients = {} +local keyPermissions = {} +local keyResponders = {} +local keyOptions = {} + +cSync = {} + +function cSync.register(key, permission, options) + check("s,?s,?t") + if permission and not aclIsValidRight(permission) then error("invalid permission format", 2) end + if keyPermissions[key] then return warn("key '"..key.."' is already registered", 2) and false end + + keyPermissions[key] = permission or GENERAL_PERMISSION + + options = options or {} + keyOptions[key] = {} + keyOptions[key].heavy = options.heavy and true or nil + keyOptions[key].irregular = options.irregular and true or nil + + return true +end + +function cSync.registerResponder(key, responder, options) + if not scheck("s,f,?t") then return false end + if not keyPermissions[key] then return warn("permission for '"..key.."' not found", 2) and false end + if keyResponders[key] then return warn("responder for '"..key.."' is already registered", 2) and false end + + keyResponders[key] = responder + + options = options or {} + keyOptions[key].auto = options.auto and true or nil + + return true +end + +function cSync.send(element, key, ...) + if not scheck("u:element:player|u:element:root,s") then return false end + if not keyPermissions[key] then return warn("permission for '"..key.."' not found", 2) and false end + + if element ~= root and not hasObjectPermissionTo(element, keyPermissions[key]) then + return warn( + string.format( + "client '%s' doesn't have permission '%s' for '%s'", + getPlayerName(element), keyPermissions[key], key + ), 2 + ) and false + end + + local irregular = keyOptions[key].irregular + local bandwidth = keyOptions[key].heavy and LATENT_BANDWIDTH or DEFAULT_BANDWIDTH + local data = table.pack(...) + + local players = element == root and cSessions.getAdmins(keyPermissions[key]) or {element} + for i, player in ipairs(players) do + if irregular and activeClients[player] or (not irregular) then + triggerLatentClientEvent(player, "gra.Sync.send", bandwidth, player, false, key, data) + end + end + + return true +end + +function cSync.callResponder(element, key, ...) + if not scheck("u:element:player|u:element:root,s") then return false end + if not keyResponders[key] then return warn("responder for '"..key.."' not found", 2) and false end + + return + select("#", ...) == 0 and + cSync.send(element, key, keyResponders[key]()) or + cSync.send(element, key, ..., keyResponders[key](...)) +end + +local function getResult(args, f) + if args.n == 0 then return table.pack(f()) end + + local result = table.pack(f(table.unpack(table.copydeep(args)))) + + for i = 1, result.n do + args[args.n + i] = result[i] + end + args.n = args.n + result.n + + return args +end + +addEventHandler("gra.cSync.gui", root, + function(state) + local client = client + if not client then return cMessages.outputSecurity("SYNC: client not found") and false end + if source ~= client then return cMessages.outputSecurity("SYNC: source $v element is not equal client $v", source, client) and false end + + activeClients[client] = state or nil + + if not state then return end + + for key, options in pairs(keyOptions) do + if options.auto and hasObjectPermissionTo(client, keyPermissions[key]) then + cSync.send(client, key, keyResponders[key]()) + end + end + + end +) + +addEventHandler("gra.cSync.request", root, + function(tag, key, data) + local client = client + if not client then return cMessages.outputSecurity("SYNC: client not found") and false end + if source ~= client then return cMessages.outputSecurity("SYNC: source $v element is not equal client $v", source, client) and false end + if not keyPermissions[key] then return cMessages.outputSecurity("SYNC: permission for $v not found (client: $v)", key, client) and false end + if not hasObjectPermissionTo(client, keyPermissions[key]) then + return cMessages.outputSecurity("SYNC: client $v doesn't have permission $v for $v", client, keyPermissions[key], key) and false + end + + if not keyResponders[key] then return warn(eventName..": responder for '"..key.."'' not found", 0) and false end + + triggerLatentClientEvent( + client, "gra.Sync.send", + keyOptions[key].heavy and LATENT_BANDWIDTH or DEFAULT_BANDWIDTH, + client, + tag, key, getResult(data, keyResponders[key]) + ) + + end +) + +addEventHandler("onPlayerQuit", root, + function() + + activeClients[source] = nil + + end +) \ No newline at end of file diff --git a/core/server/time.lua b/core/server/time.lua new file mode 100644 index 0000000..85fc1be --- /dev/null +++ b/core/server/time.lua @@ -0,0 +1,8 @@ + +addEvent("gra.cTime.sync", true, true) + +addEventHandler("gra.cTime.sync", root, + function() + triggerClientEvent(client, "gra.cTime.sync", client, getRealTime().timestamp) + end +) \ No newline at end of file diff --git a/env.lua b/env.lua new file mode 100644 index 0000000..41bb1d2 --- /dev/null +++ b/env.lua @@ -0,0 +1,8 @@ + +local env = {} +for k, v in pairs(_G) do + env[k] = v +end +env._G = env + +_MTA = env \ No newline at end of file diff --git a/gui/default.lua b/gui/default.lua new file mode 100644 index 0000000..5d77121 --- /dev/null +++ b/gui/default.lua @@ -0,0 +1,34 @@ + +local function hide(element) + + guiSetVisible(element, false) + guiSetEnabled(element, false) + guiMoveToBack(element) + + return element +end + +GUI_DEFAULT_BUTTON = hide(guiCreateButton(0, 0, 0, 0, "", false)) +GUI_DEFAULT_CHECKBOX = hide(guiCreateCheckBox(0, 0, 0, 0, "", false, false)) +GUI_DEFAULT_COMBOBOX = hide(guiCreateComboBox(0, 0, 0, 0, "", false)) +GUI_DEFAULT_EDIT = hide(guiCreateEdit(0, 0, 0, 0, "", false)) +GUI_DEFAULT_MEMO = hide(guiCreateMemo(0, 0, 0, 0, "", false)) +GUI_DEFAULT_PROGRESSBAR = hide(guiCreateProgressBar(0, 0, 0, 0, false)) +GUI_DEFAULT_RADIOBUTTON = hide(guiCreateRadioButton(0, 0, 0, 0, "", false)) +GUI_DEFAULT_SCROLLBAR = hide(guiCreateScrollBar(0, 0, 0, 0, false, false)) +GUI_DEFAULT_SCROLLPANE = hide(guiCreateScrollPane(0, 0, 0, 0, false)) +GUI_DEFAULT_STATICIMAGE = hide(guiCreateStaticImage(0, 0, 0, 0, GUI_WHITE_IMAGE_PATH, false)) +GUI_DEFAULT_LABEL = hide(guiCreateLabel(0, 0, 0, 0, "", false)) +GUI_DEFAULT_WINDOW = hide(guiCreateWindow(0, 0, 0, 0, "", false)) + +GUI_DEFAULT_GRIDLIST = hide(guiCreateGridList(0, 0, 0, 0, false)) +guiGridListAddColumn(GUI_DEFAULT_GRIDLIST, "", 0) +guiGridListAddRow(GUI_DEFAULT_GRIDLIST, "") + +GUI_DEFAULT_TABPANEL = hide(guiCreateTabPanel(0, 0, 0, 0, false)) +GUI_DEFAULT_TAB = hide(guiCreateTab("", GUI_DEFAULT_TABPANEL)) + +------------------------------------------------------------------ + +GUI_DEFAULT_HORIZONTALSEPARATOR = hide(guiCreateHorizontalSeparator(0, 0, 0, false)) +GUI_DEFAULT_VERTICALSEPARATOR = hide(guiCreateVerticalSeparator(0, 0, 0, false)) \ No newline at end of file diff --git a/gui/definitions.lua b/gui/definitions.lua new file mode 100644 index 0000000..99d7cf9 --- /dev/null +++ b/gui/definitions.lua @@ -0,0 +1,8 @@ + +GUI_SCREEN_WIDTH, GUI_SCREEN_HEIGHT = guiGetScreenSize() + +GUI_IMAGES_PATH = "gui/images/" +GUI_EMPTY_IMAGE_PATH = GUI_IMAGES_PATH.."util/empty.png" +GUI_WHITE_IMAGE_PATH = GUI_IMAGES_PATH.."util/white.png" + +GUI_FONTS_PATH = "gui/fonts/" \ No newline at end of file diff --git a/gui/fonts/DejaVuSansMono-Bold.ttf b/gui/fonts/DejaVuSansMono-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8184ced8cf853a64c3aa6f9afd722cdbf597c38c GIT binary patch literal 331992 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�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%0D`P|=8@u+UQ%=z7R+43L4Zl8#;T;K1r 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≤GWXA<}4NfYWI^m11sQ58*e$_lfk}0Xc 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

_(y_@sc** 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 literal 0 HcmV?d00001 diff --git a/gui/helpers.lua b/gui/helpers.lua new file mode 100644 index 0000000..902f095 --- /dev/null +++ b/gui/helpers.lua @@ -0,0 +1,257 @@ + +local gs = {} + +gs.header = {} +gs.header.normalColor = GS.clr.red +gs.header.font = "default-bold-small" + +gs.clickablelbl = {} +gs.clickablelbl.hoverColor = {118, 143, 246} + +gs.searchedit = {} +gs.searchedit.icon = GUI_IMAGES_PATH.."elements/search.png" + +gs.helpbutton = {} +gs.helpbutton.size = GS.h +gs.helpbutton.icon = GUI_IMAGES_PATH.."elements/info.png" +gs.helpbutton.hoverTextColor = GS.clr.aqua + +gs.acb = {} +gs.acb.color = GS.clr.white + +gs.glsearchedit = {} +gs.glsearchedit.mrg = 3 +gs.glsearchedit.h = 18 + +gs.glowbutton = {} +gs.glowbutton.normalTextColor = {255, 255, 0} +gs.glowbutton.hoverTextColor = {255, 255, 127} + +gs.glbutton = {} +gs.glbutton.mrg = 3 +gs.glbutton.h = 18 + +gs.glcreatebtn = {} +gs.glcreatebtn.w = 30 +gs.glcreatebtn.icon = GUI_IMAGES_PATH.."elements/plus.png" +gs.glcreatebtn.hoverTextColor = GS.clr.green + +gs.gldestroybtn = {} +gs.gldestroybtn.w = 30 +gs.gldestroybtn.icon = GUI_IMAGES_PATH.."elements/minus.png" +gs.gldestroybtn.hoverTextColor = GS.clr.red + + +function guiCreateHeader(x, y, width, height, text, relative, parent) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local element = guiCreateLabel(x, y, width, height, text, relative, parent) + if not element then return element end + + guiSetFont(element, gs.header.font) + guiLabelSetNormalTextColor(element, table.unpack(gs.header.normalColor)) + + return element +end + +function guiCreateClickableLabel(x, y, width, height, text, relative, parent) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local element = guiCreateLabel(x, y, width, height, text, relative, parent) + if not element then return element end + + guiLabelSetHoverColor(label, table.unpack(gs.clickablelbl.hoverColor)) + + return true +end + + +function guiLabelSetClickableStyle(label) + if not scheck("u:element:gui-label") then return false end + + guiLabelSetHoverColor(label, table.unpack(gs.clickablelbl.hoverColor)) + + return true +end + +function guiCreateSearchEdit(x, y, width, height, text, relative, parent) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local element = guiCreateEdit(x, y, width, height, text, relative, parent) + if not element then return element end + + guiEditSetIcon(element, gs.searchedit.icon) + + return element +end + +function guiButtonSetGlowStyle(button) + if not scheck("u:element:gui-button") then return false end + + return guiButtonSetStyle(button, gs.glowbutton) +end + +function guiCreateHelpButton(x, y, size, relative, parent) + if not scheck("?n[3],?b,?u:element:gui") then return false end + + size = size or gs.helpbutton.size + + local element = guiCreateButton(x, y, size, size, "", relative, parent) + if not element then return element end + + guiSetWidth(element, guiGetHeight(element, false), false) + guiButtonSetIcon(element, gs.helpbutton.icon) + guiButtonSetHoverTextColor(element, table.unpack(gs.helpbutton.hoverTextColor)) + + return element +end + +function guiCreateAdvancedComboBox(x, y, width, height, text, relative, parent) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local element = guiCreateEdit(x, y, width, height, text, relative, parent) + if not element then return element end + + guiEditSetReadOnly(element, true) + guiEditSetReadOnlyBackGroundColor(element, table.unpack(gs.acb.color)) + + return element, guiEditAddComboBox(element) +end + +function guiCreateComboBoxButton(x, y, width, height, text, relative, parent, ...) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local element = guiCreateButton(x, y, width, height, text, relative, parent, ...) + if not element then return element end + + return element, guiButtonAddComboBox(element) +end + +function guiCreateKeyValueLabels(x, y, keyWidth, valueWidth, height, keyText, valueText, relative, parent) + if not scheck("?n[5],?s[2],?b,?u:element:gui") then return false end + + local hlo = guiCreateHorizontalLayout(x, y, 0, 0, nil, relative, parent) + + local keyLabel = guiCreateLabel(0, 0, keyWidth, height, keyText, false, hlo) + guiLabelSetHorizontalAlign(keyLabel, "right") + + local valueLabel = guiCreateLabel(0, 0, valueWidth, height, valueText, false, hlo) + guiHorizontalLayoutRebuild(hlo) + + return keyLabel, valueLabel, hlo +end + +function guiCreateKeyValueEdit(x, y, keyWidth, valueWidth, height, keyText, valueText, relative, parent) + if not scheck("?n[5],?s[2],?b,?u:element:gui") then return false end + + local hlo = guiCreateHorizontalLayout(x, y, 0, 0, nil, relative, parent) + + local keyLabel = guiCreateLabel(0, 0, keyWidth, height, keyText, false, hlo) + guiLabelSetHorizontalAlign(keyLabel, "right") + + local valueEdit = guiCreateEdit(0, 0, valueWidth, height, valueText, false, hlo) + guiHorizontalLayoutRebuild(hlo) + + return keyLabel, valueEdit, hlo +end + +function guiCreateKeyValueCheckBox(x, y, keyWidth, valueWidth, height, keyText, valueText, relative, parent) + if not scheck("?n[5],?s[2],?b,?u:element:gui") then return false end + + local hlo = guiCreateHorizontalLayout(x, y, 0, 0, nil, relative, parent) + + local keyLabel = guiCreateLabel(0, 0, keyWidth, height, keyText, false, hlo) + guiLabelSetHorizontalAlign(keyLabel, "right") + + local checkbox = guiCreateCheckBox(0, 0, valueWidth, height, valueText, false, false, hlo) + guiHorizontalLayoutRebuild(hlo) + + return keyLabel, checkbox, hlo +end + +function guiCreateKeyValueColorPicker(x, y, keyWidth, valueWidth, height, keyText, r, g, b, relative, parent) + if not scheck("?n[5],?s,?byte[3],?b,?u:element:gui") then return false end + + local hlo = guiCreateHorizontalLayout(x, y, 0, 0, nil, relative, parent) + + local keyLabel = guiCreateLabel(0, 0, keyWidth, height, keyText, false, hlo) + guiLabelSetHorizontalAlign(keyLabel, "right") + + local valueColorPicker = guiCreateColorPicker(0, 0, valueWidth, height, r, g, b, false, hlo) + guiHorizontalLayoutRebuild(hlo) + + return keyLabel, valueColorPicker, hlo +end + +function guiGridListCreateSearchEdit(list) + if not scheck("u:element:gui-gridlist") then return false end + + local edit = guiCreateSearchEdit(gs.glsearchedit.mrg, gs.glsearchedit.mrg, -GS.scroll.size - gs.glsearchedit.mrg, gs.glsearchedit.h, "", false, list) + if not edit then return edit end + + guiSetRawWidth(edit, 1, true) + guiSetAlwaysOnTop(edit, true) + + return edit +end + +function guiGridListAddButton(list, text, width) + if not scheck("u:element:gui-gridlist,s,?n") then return false end + + local hlo = guiGetData(list, "buttonsLayout") + if not hlo then + hlo = guiCreateHorizontalLayout(-GS.scroll.size - gs.glbutton.mrg, gs.glbutton.mrg, nil, nil, GS.mrg, false, list) + guiSetAlwaysOnTop(hlo, true) + guiSetHorizontalAlign(hlo, "right") + guiSetData(list, "buttonsLayout", hlo) + end + + local button = guiCreateButton(nil, nil, 0, gs.glbutton.h, text, false, hlo) + if not button then return button end + + width = width or (guiGetTextExtent(button) + 20) + guiSetWidth(button, width, false) + guiHorizontalLayoutRebuild(hlo) + + return button +end + +function guiGridListAddCreateButton(list) + if not scheck("u:element:gui-gridlist") then return false end + + local button = guiGridListAddButton(list, "", gs.glcreatebtn.w) + if not button then return button end + + guiButtonSetIcon(button, gs.glcreatebtn.icon) + guiButtonSetHoverTextColor(button, unpack(gs.glcreatebtn.hoverTextColor)) + + return button +end + +function guiGridListAddDestroyButton(list) + if not scheck("u:element:gui-gridlist") then return false end + + local button = guiGridListAddButton(list, "", gs.gldestroybtn.w) + if not button then return button end + + guiButtonSetIcon(button, gs.gldestroybtn.icon) + guiButtonSetHoverTextColor(button, table.unpack(gs.gldestroybtn.hoverTextColor)) + + return button +end + +function guiCreateScrollableLabel(x, y, width, height, text, relative, parent) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local pane = guiCreateScrollPane(x, y, width, height, relative or false, parent) + if not pane then return pane end + + text = text and tostring(text) or GS.nilText + + local label = guiCreateLabel(0, 0, 1, 1, text, true, pane) + guiSetRawWidth(label, -GS.scroll.size, false) + guiLabelSetHorizontalAlign(label, "left", true) + guiLabelSetVerticalAlign(label, "top") + + return label +end \ No newline at end of file diff --git a/gui/images/colorpicker/hsvcursor.png b/gui/images/colorpicker/hsvcursor.png new file mode 100644 index 0000000000000000000000000000000000000000..fd349db55032cbd43a81d7062017f02fd74cbbaf GIT binary patch literal 3254 zcmV;n3`z5eP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0005uNklmy6@B3SQ z7dXcaN5(|M4PanlEqWyz0NExMCShPp^p=yhzXEV0dI2G=eUPSH0oOnem?Z1_-UY&) zg0;qdQN00=4amt|R=#~8nw*|C|ujxolvEYBsq0QP{e3jj0VK+-s0~Y)HZ6b->K7f#ulAHnU|;Aqwnpn4`{z zsT|$47-bcrv9g+51+!D&ah$XH&dCiFnP*SlA z1`JpMU%IY)1RMd6lO1(k_YnA;q?x~Td-OnR1=|G4ag6bDlOS>V%>Xb+vV(O=*0$}k zs;Y}klFVcBHabb$L@B`gBuc-hG?}-9x&ed`vSsKLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0000kNkl2fkY$R{{!lI24;qT3=BU9(Ek7c009600|3dS Vf&sw}BU}Ig002ovPDHLkV1h#ZIko@* literal 0 HcmV?d00001 diff --git a/gui/images/dialog/error.png b/gui/images/dialog/error.png new file mode 100644 index 0000000000000000000000000000000000000000..18e83882412c746f20799d5e1e23c023e1213b22 GIT binary patch literal 2165 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 literal 0 HcmV?d00001 diff --git a/gui/images/dialog/info.png b/gui/images/dialog/info.png new file mode 100644 index 0000000000000000000000000000000000000000..6524cba600b113eb11927ac9ff7b9752d75b2ccf GIT binary patch literal 2225 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 E0WLw=d;kCd literal 0 HcmV?d00001 diff --git a/gui/images/elements/minus.png b/gui/images/elements/minus.png new file mode 100644 index 0000000000000000000000000000000000000000..3a44abed6db5b91be6999acd9f9f4ed9d4253cc3 GIT binary patch literal 2821 zcmV+g3;OhlP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0000mNklmB6S$L(w1>EMmkaEJnJyh;cO5Mhzkn00000|NjF3 Xg@gnqC#A+H00000NkvXXu0mjf9$Ggs literal 0 HcmV?d00001 diff --git a/gui/images/elements/plus.png b/gui/images/elements/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..85e1e0ba95c4b829518cf989359dbf494bf32fd8 GIT binary patch literal 2831 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 literal 0 HcmV?d00001 diff --git a/gui/images/elements/search.png b/gui/images/elements/search.png new file mode 100644 index 0000000000000000000000000000000000000000..c1b3e455cd6d74eb655fd570872f2247c4213abf GIT binary patch literal 1091 zcmbVLO=#0l9FI(NY)p3Wq8CGk;AHu1lQvnqt!-LYu(NiB^`>d^y2dSeY4WzF!^7Av zQ+AMf69w_0Lu7iM;`AiS6ufv4@!-jWFc9WR(3gHp4`Yad z2%;}Lt`_imEO@(*;P0t%`aNDwpmYgM!V0QuE+7&HECVuYY15zpG^0^_4Ppe*HE$M6 zsFXXe=+I)c0K@o}gV_WT8}%Jcp8<$0gK5)_Q#(($DAF|I)RdUxa!v|V&G7{nOfF0m z^@SN-HmK1NGUhATfCZ37`qr%NDSn)4+EwsAxMnG`2|+V)suffzmnT!u1*FJGw9bb` zG9oj)6bj4o5Gincl;xrTPavhF1Kgc_S%KQ zC1ZWfVR?pQEi1^YiS|$d{0U=Uv{!66fGq$I&bd17M`fTx#LB4H$|IO zp+08;8)elvh5s;yX((Y(iArHzpn)!kbU75}>2ihRXpOH(vRDy0AtH6`?8lF)LWWCZ zR9;F&c|M(#lj&$CqlPkxP$V6Z5*=>V_K;@lpyS)bzT4c?fm|i!0u4d82;ppJ3i4Hm zpjU+snMws+m&r3zrftBwHyE_3m9+|7a~>EO7g}Vq&WgED4F8sYi*NjI{a73n8w|@| z!_&FK6B%sxq<{~5Vgfc+kc*`vu0LAEGSjnaqUe9Tz1G{aIM%J^etd4!#Is93hlal` zu6*gL#Xj9xGcImzzPz+DmA%!txBNZd`}pqQ>KMI!_|0~*Z+ZLK(T7)NUVW(b4_|-q swd=S~?CkEoyK(5(re*YRJb8Qi1u?RGb-j51=JP<-+4O|Ek-WV08{L3k(EtDd literal 0 HcmV?d00001 diff --git a/gui/images/util/empty.png b/gui/images/util/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..2243795af07661d2d48bdadf91b43ea1ac1ea9be GIT binary patch literal 959 zcmV;w13>(VP)U8P*7-ZbZ>KLZ*U+lnSp_Ufq@}0xwybFAi#%#fq@|}KQEO56)-X|e7nZL z$iTqBa9P*U#mSX{G{Bl%P*lRez;J+pfx##xwK$o9f#C}S14DXwNkIt%17i#W1A|CX zc0maP17iUL1A|C*NRTrF17iyV0~1e4YDEbH0|SF|enDkXW_m`6f}y3QrGjHhep0GJ zaAk2xYHqQDXI^rCQ9*uDVo7QW0|Nup4h9AW240u^5(W3f%sd4n162kpgNVo|1qcff zJ_s=cNG>fZg9jx8g8+j9g8_pBLjXe}Lp{R+hNBE`7{wV~7)u#fFy3PlV+vxLz;uCG zm^qSpA@ds+OO_6nTdaDlt*rOhEZL^9ePa)2-_4=K(Z%tFGm-NGmm}8}ZcXk5JW@PU zd4+f<@d@)yL(o<5icqT158+-B6_LH7;i6x}CW#w~Uy-Pgl#@Irl`kzV zeL|*8R$ca%T%Wv){2zs_iiJvgN^h0dsuZZ2sQy$tsNSU!s;Q*;LF<6_B%M@UD?LHI zSNcZ`78uqV#TeU~$eS{ozBIdFzSClfs*^S+dw;4dus<{M;#|MXC)T}S9v!D zcV!QCPhBq)ZyO(X-(bH4|NMaZz==UigLj2o41F2S6d@OB6%`R(5i>J(Puzn9wnW{e zu;hl6HK{k#IWjCVGqdJqU(99Cv(K+6*i`tgSi2;vbXD1#3jNBGs$DgVwO(~o>mN4i zHPtkqZIx>)Y(Ls5-Br|mx>vQYvH$Kwn@O`L|D75??eGkZnfg$5<;Xeg_o%+-I&+-3%01W^SH2RkDT>t<81ZP1_K>z@;j(q!3lK=n! zAY({UO#lFTB>(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fdMgRZ+5lKWrRCwBA h_|E_U009600{{X60RWj)x@`ae002ovPDHLkV1i1pi}3&e literal 0 HcmV?d00001 diff --git a/gui/images/util/white.png b/gui/images/util/white.png new file mode 100644 index 0000000000000000000000000000000000000000..818c71d03f435db011069584cda25c1f66af1a85 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2s6ii6yp7}lMWc?smOq&xaLGB9lH z=l+w(3gmMZctjR6Fz_7)VaDV6D^h@hJf1F&Arj%qKmPx>XJGxu^l!OoV+&B6!PC{x JWt~$(69DNq9##MV literal 0 HcmV?d00001 diff --git a/gui/lib/bind.lua b/gui/lib/bind.lua new file mode 100644 index 0000000..bc1383e --- /dev/null +++ b/gui/lib/bind.lua @@ -0,0 +1,51 @@ + +local Bind = {} + +Bind.elementKeys = {} + +function Bind.getElements(key) + + local elements = {} + for element, keys in pairs(Bind.elementKeys) do + if table.index(keys, key) then table.insert(elements, element) end + end + return elements +end + +addEventHandler("onClientElementDestroy", guiRoot, + function() + + Bind.elementKeys[source] = nil + + end +) + +addEventHandler("onClientKey", root, + function(button, press) + if guiGetInputEnabled() then return end + if not press then return end + + for i, element in ipairs(Bind.getElements(button)) do + if guiGetFocus(guiGetGreatParent(element)) then guiClick(element) end + end + + end +) + +function guiBindKey(element, key) + if not scheck("u:element:gui,s") then return false end + + local keys = Bind.elementKeys[element] or {} + if table.index(keys, key) then return false end + + table.insert(keys, key) + Bind.elementKeys[element] = keys + + return true +end + +function guiGetBoundKeys(element) + if not scheck("u:element:gui,s") then return false end + + return table.copy(Bind.elementKeys[element] or {}) +end diff --git a/gui/lib/button.lua b/gui/lib/button.lua new file mode 100644 index 0000000..5800258 --- /dev/null +++ b/gui/lib/button.lua @@ -0,0 +1,42 @@ + +function guiButtonSetHoverTextColor(button, r, g, b) + if not scheck("u:element:gui-button,byte[3]") then return false end + + return guiSetColorProperty(button, "HoverTextColour", r, g, b) +end + +function guiButtonGetHoverTextColor(button) + if not scheck("u:element:gui-button") then return false end + + return guiGetColorProperty(button, "HoverTextColour") +end + +function guiButtonSetNormalTextColor(button, r, g, b) + if not scheck("u:element:gui-button,byte[3]") then return false end + + return guiSetColorProperty(button, "NormalTextColour", r, g, b) +end + +function guiButtonGetNormalTextColor(button) + if not scheck("u:element:gui-button") then return false end + + return guiGetColorProperty(button, "NormalTextColour") +end + +function guiButtonSetDisabledTextColor(button, r, g, b) + if not scheck("u:element:gui-button,byte[3]") then return false end + + return guiSetColorProperty(button, "DisabledTextColour", r, g, b) +end + +function guiButtonGetDisabledTextColor(button) + if not scheck("u:element:gui-button") then return false end + + return guiGetColorProperty(button, "DisabledTextColour") +end + +function guiButtonGetCurrentTextColor(button) + if not scheck("u:element:gui-button") then return false end + + return guiGetColorProperty(button, guiGetEnabled(button) and "NormalTextColour" or "DisabledTextColour") +end \ No newline at end of file diff --git a/gui/lib/button_combobox.lua b/gui/lib/button_combobox.lua new file mode 100644 index 0000000..a79ede9 --- /dev/null +++ b/gui/lib/button_combobox.lua @@ -0,0 +1,78 @@ + +local DROPDOWN_IMG_PATH = GUI_IMAGES_PATH.."/elements/dropdown.png" + +local ButtonComboBox = {} + +function ButtonComboBox.createButton(button) + + local height = guiGetHeight(button, false) + local specialButton = guiCreateButton(0, 0, height, height, "", false, button) + guiSetHeight(specialButton, 1, true) + guiSetHorizontalAlign(specialButton, "right") + guiSetVerticalAlign(specialButton, "center") + + return specialButton +end + +function ButtonComboBox.onListItemSelected(row) + + local itemText = row > -1 and guiGridListGetItemText(this, row) or NIL_TEXT + local button = getElementParent(this) + guiSetText(button, guiGetInternalData(button, "combobox.prefix")..itemText) + +end + +function ButtonComboBox.onListAccept() + + guiSetVisible(this, false) + +end + +function ButtonComboBox.onButtonBlur() + + guiSetVisible(guiGetInternalData(this, "combobox.list"), false) + +end + +function ButtonComboBox.onListButtonClick(key) + if key ~= "left" then return end + + local button = getElementParent(this) + local list = guiGetInternalData(button, "combobox.list") + if guiGetVisible(list) then return guiSetVisible(list, false) end + if not guiGetEnabled(button) then return end + + guiSetVisible(list, true, true) + +end + +function guiButtonAddComboBox(button) + if not scheck("u:element:gui-button") then return false end + if guiGetInternalData(button, "combobox.list") then return false end + + local listButton = ButtonComboBox.createButton(button) + guiButtonSetIcon(listButton, DROPDOWN_IMG_PATH) + + local list = guiCreateGridList(0, 1, 1, 0, true, button) + guiSetRawYPosition(list, 1, false) -- offset 1px + + guiSetProperty(list, "ClippedByParent", "False") + guiGridListAdjustHeight(list, 0) + guiGridListSetSortingEnabled(list, false) + guiGridListAddColumn(list, "", 1) + guiGridListAdjustColumnWidth(list) + guiSetVisible(list, false) + + local text = utf8.trim(guiGetText(button)) + guiSetInternalData(button, "combobox.prefix", text ~= "" and text..": " or "") + guiSetInternalData(button, "combobox.button", listButton) + guiSetInternalData(button, "combobox.list", list) + + addEventHandler("onClientGUIGridListItemSelected", list, ButtonComboBox.onListItemSelected, false) + addEventHandler("onClientGUIDoubleLeftClick", list, ButtonComboBox.onListAccept, false) + addEventHandler("onClientGUIGridListItemEnterKey", list, ButtonComboBox.onListAccept, false) + addEventHandler("onClientGUIBlur", button, ButtonComboBox.onButtonBlur, false) + addEventHandler("onClientGUIClick", listButton, ButtonComboBox.onListButtonClick, false) + + return list +end \ No newline at end of file diff --git a/gui/lib/button_icon.lua b/gui/lib/button_icon.lua new file mode 100644 index 0000000..e1bb6f9 --- /dev/null +++ b/gui/lib/button_icon.lua @@ -0,0 +1,39 @@ + +local ButtonIcon = {} + +function ButtonIcon.onButtonColorChange() + if not guiIsGrandParentFor(source, this) then return end + + local icon = guiGetInternalData(this, "icon") + if not icon then return end + + local r, g, b, a = guiButtonGetNormalTextColor(this) + if eventName == "onClientMouseEnter" then + r, g, b, a = guiButtonGetHoverTextColor(this) + elseif eventName == "onClientGUIDisabled" then + r, g, b, a = guiButtonGetDisabledTextColor(this) + end + guiStaticImageSetColor(icon, r, g, b, a) + +end + +function guiButtonSetIcon(button, path, align) + if not scheck("u:element:gui-button,?s,?s") then return false end + + local img = guiCreateStaticImage(0, 0, 0, 0, path, false, button) + if not img then return false end + + guiSetEnabled(img, false) + guiStaticImageSetSizeNative(img) + guiSetVerticalAlign(img, "center") + guiSetHorizontalAlign(img, align or "center") + guiStaticImageSetColor(img, guiButtonGetCurrentTextColor(button)) + guiSetInternalData(button, "icon", img) + + addEventHandler("onClientMouseEnter", button, ButtonIcon.onButtonColorChange, false) + addEventHandler("onClientMouseLeave", button, ButtonIcon.onButtonColorChange, false) + addEventHandler("onClientGUIEnabled", button, ButtonIcon.onButtonColorChange) + addEventHandler("onClientGUIDisabled", button, ButtonIcon.onButtonColorChange) + + return true +end \ No newline at end of file diff --git a/gui/lib/checkbox.lua b/gui/lib/checkbox.lua new file mode 100644 index 0000000..7b9a0ad --- /dev/null +++ b/gui/lib/checkbox.lua @@ -0,0 +1,8 @@ + +function guiCheckBoxAdjustWidth(checkbox) + if not scheck("u:element:gui-checkbox") then return false end + + local width = guiGetTextExtent(checkbox) + + return guiSetWidth(checkbox, width + 18, false) +end \ No newline at end of file diff --git a/gui/lib/combobox.lua b/gui/lib/combobox.lua new file mode 100644 index 0000000..e45c1e2 --- /dev/null +++ b/gui/lib/combobox.lua @@ -0,0 +1,9 @@ + +function guiComboBoxAdjustHeight(comboBox, itemCount) + if not scheck("u:element:gui-combobox,?n") then return false end + + if not itemCount then itemCount = 1 end + local width = guiGetSize(comboBox, false) + + return guiSetSize(comboBox, width, itemCount * 14 + 38, false) +end \ No newline at end of file diff --git a/gui/lib/customproperty.lua b/gui/lib/customproperty.lua new file mode 100644 index 0000000..0122052 --- /dev/null +++ b/gui/lib/customproperty.lua @@ -0,0 +1,29 @@ + +local properties = {} + +function guiSetCustomProperty(element, key, value) + if not scheck("u:element:gui,s") then return false end + + local elementProperties = properties[element] or {} + if elementProperties[key] == value then return false end + + elementProperties[key] = value + properties[element] = elementProperties + + return true +end + +function guiGetCustomProperty(element, key) + if not scheck("u:element:gui,s") then return false end + + if not properties[element] then return nil end + return properties[element][key] +end + +addEventHandler("onClientElementDestroy", guiRoot, + function() + properties[source] = nil + end, + true, + "low" +) \ No newline at end of file diff --git a/gui/lib/data.lua b/gui/lib/data.lua new file mode 100644 index 0000000..4d9e973 --- /dev/null +++ b/gui/lib/data.lua @@ -0,0 +1,29 @@ + +local data = {} + +function guiSetData(element, key, value) + if not scheck("u:element:gui,s") then return false end + + local elementData = data[element] or {} + if elementData[key] == value then return false end + + elementData[key] = value + data[element] = elementData + + return true +end + +function guiGetData(element, key) + if not scheck("u:element:gui,s") then return false end + + if not data[element] then return nil end + return data[element][key] +end + +addEventHandler("onClientElementDestroy", guiRoot, + function() + data[source] = nil + end, + true, + "low" +) \ No newline at end of file diff --git a/gui/lib/edit.lua b/gui/lib/edit.lua new file mode 100644 index 0000000..fa8b5fc --- /dev/null +++ b/gui/lib/edit.lua @@ -0,0 +1,48 @@ + +addEventHandler("onClientKey", root, + function(key, press) + if key ~= "tab" then return end + if not press then return end + if not sourceGUIInput then return end + if getElementType(sourceGUIInput) ~= "gui-edit" then return end + + guiBlur(sourceGUIInput) + end +) + +local _guiCreateEdit = guiCreateEdit +function guiCreateEdit(x, y, w, h, text, relative, parent) + if not scheck("n[4],s,b,?u:element:gui") then return false end + + local edit = _guiCreateEdit(x, y, w, h, text, relative, parent) + if not edit then return edit end + + addEventHandler("onClientGUIAccepted", edit, function() guiBlur(edit) end, true, "high") -- or false? + + return edit +end + +local _guiEditSetCaretIndex = guiEditSetCaretIndex +function guiEditSetCaretIndex(edit, index) + if not scheck("u:element:gui-edit,?n") then return false end + + return _guiEditSetCaretIndex(edit, index or #(guiGetText(edit))) +end + +function guiEditSetValidationString(edit, str) + if not scheck("u:element:gui-edit,s") then return false end + + return guiSetProperty(edit, "ValidationString", str) +end + +function guiEditSetReadOnlyBackGroundColor(edit, r, g, b, a) + if not scheck("u:element:gui-edit,n[3],?n") then return false end + + guiSetColorProperty(edit, "ReadOnlyBGColour", r, g, b, a) + + -- NEEDED (CEGUI bug maybe) + if guiEditIsReadOnly(edit) then guiBlur(edit) end + + return true +end + diff --git a/gui/lib/edit_combobox.lua b/gui/lib/edit_combobox.lua new file mode 100644 index 0000000..022c4d3 --- /dev/null +++ b/gui/lib/edit_combobox.lua @@ -0,0 +1,90 @@ + +local DROPDOWN_IMG_PATH = GUI_IMAGES_PATH.."/elements/dropdown.png" + +local EditComboBox = {} + +function EditComboBox.createButton(edit) + + local height = guiGetHeight(edit, false) + local button = guiCreateButton(0, 0, height, height, "", false, edit) + guiSetHeight(button, 1, true) + guiSetHorizontalAlign(button, "right") + guiSetVerticalAlign(button, "center") + guiSetInheritsAlpha(button, false) + + return button +end + +function EditComboBox.onListItemSelected(row) + + local itemText = row > -1 and guiGridListGetItemText(this, row) or nil + local edit = getElementParent(this) + guiSetText(edit, itemText or "") + +end + +function EditComboBox.onListDoubleClick(key, state) + if key ~= "left" then return end + + guiSetVisible(this, false) + +end + +function EditComboBox.onEditBlur() + + guiSetVisible(guiGetInternalData(this, "combobox.list"), false) + +end + +function EditComboBox.onListButtonClick(key, state) + if key ~= "left" then return end + + local edit = getElementParent(this) + local list = guiGetInternalData(edit, "combobox.list") + if guiGetVisible(list) then return guiSetVisible(list, false) end + if not guiGetEnabled(edit) then return end + + guiSetVisible(list, true, true) + +end + +addEventHandler("onClientKey", root, + function(key, down) + if key ~= "enter" then return end + if not down then return end + --for cbb, data in pairs(cbbsData) do + -- if guiGetVisible(data.list) then + -- guiBlur(cbb) + -- end + --end + end +) + +function guiEditAddComboBox(edit) + if not scheck("u:element:gui-edit") then return false end + if guiGetInternalData(edit, "combobox.list") then return false end + + local listButton = EditComboBox.createButton(edit) + guiButtonSetIcon(listButton, DROPDOWN_IMG_PATH) + + local list = guiCreateGridList(0, 1, 1, 0, true, edit) + guiSetRawYPosition(list, 1, false) -- offset 1px + + guiSetProperty(list, "ClippedByParent", "False") + guiGridListAdjustHeight(list, 0) + guiGridListSetSortingEnabled(list, false) + guiGridListAddColumn(list, "", 1) + guiGridListAdjustColumnWidth(list) + guiSetVisible(list, false) + + guiSetInternalData(edit, "combobox.text", guiGetText(edit)) + guiSetInternalData(edit, "combobox.edit", listButton) + guiSetInternalData(edit, "combobox.list", list) + + addEventHandler("onClientGUIGridListItemSelected", list, EditComboBox.onListItemSelected, false) + addEventHandler("onClientGUIDoubleClick", list, EditComboBox.onListDoubleClick, false) + addEventHandler("onClientGUIBlur", edit, EditComboBox.onEditBlur, false) + addEventHandler("onClientGUIClick", listButton, EditComboBox.onListButtonClick, false) + + return list +end \ No newline at end of file diff --git a/gui/lib/edit_icon.lua b/gui/lib/edit_icon.lua new file mode 100644 index 0000000..aa4cce5 --- /dev/null +++ b/gui/lib/edit_icon.lua @@ -0,0 +1,20 @@ + +function guiEditSetIcon(edit, path, r, g, b, a) + if not scheck("u:element:gui-edit,s,?n[4]") then return false end + + r = r and math.clamp(r, 0, 255) or 0 + g = g and math.clamp(g, 0, 255) or 0 + b = b and math.clamp(b, 0, 255) or 0 + a = a and math.clamp(a, 0, 255) or 255 + + local height = guiGetHeight(edit, false) + local img = guiCreateStaticImage(-height * 0.25, 0, 0, 0, path, false, edit) + guiSetEnabled(img, false) + guiStaticImageSetSizeNative(img) + guiSetVerticalAlign(img, "center") + guiSetHorizontalAlign(img, "right") + guiStaticImageSetColor(img, r, g, b, a) + guiSetInternalData(edit, "icon", img) + + return img +end \ No newline at end of file diff --git a/gui/lib/edit_matcher.lua b/gui/lib/edit_matcher.lua new file mode 100644 index 0000000..251a4d4 --- /dev/null +++ b/gui/lib/edit_matcher.lua @@ -0,0 +1,45 @@ + +local EditMatcher = {} + +function EditMatcher.onChanged() + + local matcher = guiGetInternalData(this, "matcher") + if not matcher then return end + + guiSetText(this, matcher(guiGetText(this)) or "") + +end + +function EditMatcher.onBlur() + + local matcher = guiGetInternalData(this, "matcher") + if not matcher then return end + + guiSetText(this, matcher(guiGetText(this), true) or "") + +end + +local _guiCreateEdit = guiCreateEdit +function guiCreateEdit(x, y, w, h, text, relative, parent) + if not scheck("n[4],s,b,?u:element:gui") then return false end + + local edit = _guiCreateEdit(x, y, w, h, text, relative, parent) + if not edit then return edit end + + addEventHandler("onClientGUIChanged", edit, EditMatcher.onChanged, false, "high") + addEventHandler("onClientGUIBlur", edit, EditMatcher.onBlur, false, "high+1") + + return edit +end + +function guiEditSetMatcher(edit, matcher) + if not scheck("u:element:gui-edit,?f") then return false end + + return guiSetInternalData(edit, "matcher", matcher) +end + +function guiEditGetMatcher(edit) + if not scheck("u:element:gui-edit") then return false end + + return guiGetInternalData(edit, "matcher") +end \ No newline at end of file diff --git a/gui/lib/edit_parser.lua b/gui/lib/edit_parser.lua new file mode 100644 index 0000000..4ca4c4a --- /dev/null +++ b/gui/lib/edit_parser.lua @@ -0,0 +1,22 @@ + +function guiEditSetParser(edit, parser) + if not scheck("u:element:gui-edit,?f") then return false end + + return guiSetInternalData(edit, "parser", parser) +end + +function guiEditGetParser(edit) + if not scheck("u:element:gui-edit") then return false end + + return guiGetInternalData(edit, "parser") +end + +function guiEditGetParsedValue(edit) + if not scheck("u:element:gui-edit") then return false end + + local text = guiGetText(edit) + local parser = guiGetInternalData(edit, "parser") + if not parser then return text end + + return parser(text) +end \ No newline at end of file diff --git a/gui/lib/elements/container.lua b/gui/lib/elements/container.lua new file mode 100644 index 0000000..27e24a6 --- /dev/null +++ b/gui/lib/elements/container.lua @@ -0,0 +1,11 @@ + +function guiCreateContainer(x, y, w, h, relative, parent) + if not scheck("n[4],b,?u:element:gui") then return false end + + local container = guiCreateStaticImage(x, y, w, h, GUI_WHITE_IMAGE_PATH, relative, parent) + if not container then return false end + + guiStaticImageSetColor(container, 0, 0, 0, 0) + + return container +end \ No newline at end of file diff --git a/gui/lib/elements/gridlayout.lua b/gui/lib/elements/gridlayout.lua new file mode 100644 index 0000000..6747027 --- /dev/null +++ b/gui/lib/elements/gridlayout.lua @@ -0,0 +1,134 @@ + +local GridLayout = {} + +GridLayout.layouts = {} + +function GridLayout.onDestroy() + + table.removevalue(GridLayout.layouts, this) + +end + +function GridLayout.create(x, y, width, height, columns, spacing, relative, parent) + + local layout = guiCreateContainer(x, y, width, height, relative, parent) + + guiSetInternalData(layout, "columns", columns) + guiSetInternalData(layout, "spacing", spacing) + guiSetInternalData(layout, "horizontalAlign", "center") + guiSetInternalData(layout, "verticalAlign", "center") + table.insert(GridLayout.layouts, layout) + + addEventHandler("onClientElementDestroy", layout, GridLayout.onDestroy, false) + + return layout +end + +function GridLayout.rebuild(layout) + + local columns = guiGetInternalData(layout, "columns") + local spacing = guiGetInternalData(layout, "spacing") + local horizontalAlign = guiGetInternalData(layout, "horizontalAlign") + local verticalAlign = guiGetInternalData(layout, "verticalAlign") + + local components = getElementChildren(layout) + if #components == 0 then + guiSetWidth(layout, 0, false) + guiSetHeight(layout, 0, false) + return true + end + + local maxw, maxh = 0, 0 + for i, element in ipairs(components) do + maxw = math.max(maxw, guiGetWidth(element, false)) + maxh = math.max(maxh, guiGetHeight(element, false)) + end + local lw = (maxw + spacing) * (columns - 1) + maxw + local lh = (maxh + spacing) * (math.ceil(#components/columns) - 1) + maxh + + local x, y = 0, 0 + local row, column = 1, 1 + for i, element in ipairs(components) do + + local w, h = guiGetWidth(element, false), guiGetHeight(element, false) + local ax = + horizontalAlign == "right" and (x + maxw - w) or + horizontalAlign == "center" and (x + (maxw - w)*0.5) or + x + local ay = + verticalAlign == "bottom" and (y + maxh - h) or + verticalAlign == "center" and (y + (maxh - h)*0.5) or + y + guiSetXPosition(element, ax, false) + guiSetYPosition(element, ay, false) + guiSetHorizontalAlign(element, "left") + guiSetVerticalAlign(element, "top") + + if column + 1 > columns then + column = 1 + row = row + 1 + x = 0 + y = y + maxh + spacing + else + column = column + 1 + x = x + maxw + spacing + end + end + + guiSetMaxSize(layout, lw, lh, false) + guiSetSize(layout, lw, lh, false) + + return true +end + +function guiCreateGridLayout(x, y, width, height, columns, spacing, relative, parent) + if not scheck("n[6],b,?u:element:gui") then return false end + + return GridLayout.create(x, y, width, height, columns, spacing, relative, parent) +end + +function guiIsGridLayout(element) + if not scheck("u:element:gui") then return false end + + return table.index(GridLayout.layouts, element) and true or false +end + +function guiGridLayoutRebuild(layout) + if not scheck("u:element:gui") then return false end + if not table.index(GridLayout.layouts, layout) then return false end + + return GridLayout.rebuild(layout) +end + +function guiGridLayoutSetColumns(layout, columns) + if not scheck("u:element:gui,n") then return false end + if not table.index(GridLayout.layouts, layout) then return false end + + columns = math.floor(columns) + if columns < 1 then return false end + + guiSetInternalData(layout, "columns", columns) + GridLayout.rebuild(layout) + + return true +end + +function guiGridLayoutSetHorizontalAlign(layout, align) + if not scheck("u:element:gui,s") then return false end + if not table.index(GridLayout.layouts, layout) then return false end + + guiSetInternalData(layout, "horizontalAlign", align) + GridLayout.rebuild(layout) + + return true +end + +function guiGridLayoutSetVerticalAlign(layout, align) + if not scheck("u:element:gui,s") then return false end + if not table.index(GridLayout.layouts, layout) then return false end + + guiSetInternalData(layout, "verticalAlign", align) + GridLayout.rebuild(layout) + + return true +end \ No newline at end of file diff --git a/gui/lib/elements/horizontallayout.lua b/gui/lib/elements/horizontallayout.lua new file mode 100644 index 0000000..d7daa43 --- /dev/null +++ b/gui/lib/elements/horizontallayout.lua @@ -0,0 +1,102 @@ + +local HorizontalLayout = {} + +HorizontalLayout.layouts = {} + +function HorizontalLayout.onDestroy() + + table.removevalue(HorizontalLayout.layouts, this) + +end + +function HorizontalLayout.create(x, y, width, height, spacing, relative, parent) + + local layout = guiCreateContainer(x, y, width, height, relative, parent) + + guiSetInternalData(layout, "spacing", spacing) + guiSetInternalData(layout, "verticalAlign", "top") + table.insert(HorizontalLayout.layouts, layout) + addEventHandler("onClientElementDestroy", layout, HorizontalLayout.onDestroy, false) + + return layout +end + +function HorizontalLayout.rebuild(layout) + + local verticalAlign = guiGetInternalData(layout, "verticalAlign") + local spacing = guiGetInternalData(layout, "spacing") + + local x, rx = 0, 0 + local maxh = 0 + + local components = getElementChildren(layout) + if #components > 0 then + for i, element in ipairs(components) do + + guiSetRawPosition(element, x, 0, false) + guiSetRawPosition(element, rx, 0, true) + + guiSetHorizontalAlign(element, "left") + guiSetVerticalAlign(element, verticalAlign) + + x = x + guiGetRawWidth(element, false) + spacing + rx = rx + guiGetRawWidth(element, true) + maxh = math.max(maxh, guiGetHeight(element, false)) + end + x = x - spacing + end + + if not guiGetInternalData(layout, "fixedWidth") then guiSetWidth(layout, x, false) end + if not guiGetInternalData(layout, "fixedHeight") then guiSetHeight(layout, maxh, false) end + + return true +end + +function guiCreateHorizontalLayout(x, y, width, height, spacing, relative, parent) + if not scheck("n[5],b,?u:element:gui") then return false end + + return HorizontalLayout.create(x, y, width, height, spacing, relative, parent) +end + +function guiIsHorizontalLayout(element) + if not scheck("u:element:gui") then return false end + + return table.index(HorizontalLayout.layouts, element) and true or false +end + +function guiHorizontalLayoutRebuild(layout) + if not scheck("u:element:gui") then return false end + if not table.index(HorizontalLayout.layouts, layout) then return false end + + return HorizontalLayout.rebuild(layout) +end + +function guiHorizontalLayoutSetVerticalAlign(layout, align) + if not scheck("u:element:gui,s") then return false end + if not table.index(HorizontalLayout.layouts, layout) then return false end + + return guiSetInternalData(layout, "verticalAlign", align) +end + +function guiHorizontalLayoutSetWidthFixed(layout, state) + if not scheck("u:element:gui,b") then return false end + if not table.index(HorizontalLayout.layouts, layout) then return false end + + return guiSetInternalData(layout, "fixedWidth", state) +end + +function guiHorizontalLayoutSetHeightFixed(layout, state) + if not scheck("u:element:gui,b") then return false end + if not table.index(HorizontalLayout.layouts, layout) then return false end + + return guiSetInternalData(layout, "fixedHeight", state) +end + +function guiHorizontalLayoutSetSizeFixed(layout, state) + if not scheck("u:element:gui,b") then return false end + if not table.index(HorizontalLayout.layouts, layout) then return false end + + guiSetInternalData(layout, "fixedWidth", state) + guiSetInternalData(layout, "fixedHeight", state) + return true +end \ No newline at end of file diff --git a/gui/lib/elements/layout.lua b/gui/lib/elements/layout.lua new file mode 100644 index 0000000..5c68a8e --- /dev/null +++ b/gui/lib/elements/layout.lua @@ -0,0 +1,26 @@ + +local Layout = {} + +function Layout.rebuild(layout) + + if guiIsHorizontalLayout(layout) then return guiHorizontalLayoutRebuild(layout) end + if guiIsVerticalLayout(layout) then return guiVerticalLayoutRebuild(layout) end + if guiIsGridLayout(layout) then return guiGridLayoutRebuild(layout) end + + return false +end + +function Layout.rebuildAll(element) + + for i, child in ipairs(getElementChildren(element)) do + Layout.rebuildAll(child) + end + + return Layout.rebuild(element) +end + +function guiRebuildLayouts(element) + if not scheck("u:element:gui") then return false end + + return Layout.rebuildAll(element) +end \ No newline at end of file diff --git a/gui/lib/elements/separator.lua b/gui/lib/elements/separator.lua new file mode 100644 index 0000000..ab0591c --- /dev/null +++ b/gui/lib/elements/separator.lua @@ -0,0 +1,35 @@ + +local gs = {} +gs.clr = COLORS.grey + +function guiCreateHorizontalSeparator(x, y, length, relative, parent) + if not scheck("n[3],b,?u:element:gui") then return false end + + local image = guiCreateStaticImage(x, y, length, 0, GUI_WHITE_IMAGE_PATH, relative, parent) + guiSetHeight(image, 1, false) + guiStaticImageSetColor(image, table.unpack(gs.clr)) + + return image +end + +function guiCreateVerticalSeparator(x, y, length, relative, parent) + if not scheck("n[3],b,?u:element:gui") then return false end + + local image = guiCreateStaticImage(x, y, 0, length, GUI_WHITE_IMAGE_PATH, relative, parent) + guiSetWidth(image, 1, false) + guiStaticImageSetColor(image, table.unpack(gs.clr)) + + return image +end + +function guiSeparatorGetColor(separator) + if not scheck("u:element:gui-staticimage") then return false end + + return guiStaticImageGetColor(separator) +end + +function guiSeparatorSetColor(separator, r, g, b, a) + if not scheck("u:element:gui-staticimage,n[3],?n") then return false end + + return guiStaticImageSetColor(separator, r, g, b, a) +end \ No newline at end of file diff --git a/gui/lib/elements/verticallayout.lua b/gui/lib/elements/verticallayout.lua new file mode 100644 index 0000000..18db449 --- /dev/null +++ b/gui/lib/elements/verticallayout.lua @@ -0,0 +1,102 @@ + +local VerticalLayout = {} + +VerticalLayout.layouts = {} + +function VerticalLayout.onDestroy() + + table.removevalue(VerticalLayout.layouts, this) + +end + +function VerticalLayout.create(x, y, width, height, spacing, relative, parent) + + local layout = guiCreateContainer(x, y, width, height, relative, parent) + + guiSetInternalData(layout, "spacing", spacing) + guiSetInternalData(layout, "horizontalAlign", "left") + table.insert(VerticalLayout.layouts, layout) + addEventHandler("onClientElementDestroy", layout, VerticalLayout.onDestroy, false) + + return layout +end + +function VerticalLayout.rebuild(layout) + + local horizontalAlign = guiGetInternalData(layout, "horizontalAlign") + local spacing = guiGetInternalData(layout, "spacing") + + local y, ry = 0, 0 + local maxw = 0 + + local components = getElementChildren(layout) + if #components > 0 then + for i, element in ipairs(components) do + + guiSetRawPosition(element, 0, y, false) + guiSetRawPosition(element, 0, ry, true) + + guiSetHorizontalAlign(element, horizontalAlign) + guiSetVerticalAlign(element, "top") + + y = y + guiGetRawHeight(element, false) + spacing + ry = ry + guiGetRawHeight(element, true) + maxw = math.max(maxw, guiGetWidth(element, false)) + end + y = y - spacing + end + + if not guiGetInternalData(layout, "fixedWidth") then guiSetWidth(layout, maxw, false) end + if not guiGetInternalData(layout, "fixedHeight") then guiSetHeight(layout, y, false) end + + return true +end + +function guiCreateVerticalLayout(x, y, width, height, spacing, relative, parent) + if not scheck("n[5],b,?u:element:gui") then return false end + + return VerticalLayout.create(x, y, width, height, spacing, relative, parent) +end + +function guiIsVerticalLayout(element) + if not scheck("u:element:gui") then return false end + + return table.index(VerticalLayout.layouts, element) and true or false +end + +function guiVerticalLayoutRebuild(layout) + if not scheck("u:element:gui") then return false end + if not table.index(VerticalLayout.layouts, layout) then return false end + + return VerticalLayout.rebuild(layout) +end + +function guiVerticalLayoutSetHorizontalAlign(layout, align) + if not scheck("u:element:gui,s") then return false end + if not table.index(VerticalLayout.layouts, layout) then return false end + + return guiSetInternalData(layout, "horizontalAlign", align) +end + +function guiVerticalLayoutSetWidthFixed(layout, state) + if not scheck("u:element:gui,b") then return false end + if not table.index(VerticalLayout.layouts, layout) then return false end + + return guiSetInternalData(layout, "fixedWidth", state) +end + +function guiVerticalLayoutSetHeightFixed(layout, state) + if not scheck("u:element:gui,b") then return false end + if not table.index(VerticalLayout.layouts, layout) then return false end + + return guiSetInternalData(layout, "fixedHeight", state) +end + +function guiVerticalLayoutSetSizeFixed(layout, fixed) + if not scheck("u:element:gui,b") then return false end + if not table.index(VerticalLayout.layouts, layout) then return false end + + guiSetInternalData(layout, "fixedWidth", true) + guiSetInternalData(layout, "fixedHeight", true) + return true +end \ No newline at end of file diff --git a/gui/lib/events.lua b/gui/lib/events.lua new file mode 100644 index 0000000..707ac0c --- /dev/null +++ b/gui/lib/events.lua @@ -0,0 +1,74 @@ + +addEvent("onClientGUIEnabled", false) +addEvent("onClientGUIDisabled", false) +addEvent("onClientGUIShow", false) +addEvent("onClientGUIHide", false) +addEvent("onClientGUILeftClick", false) +addEvent("onClientGUIRightClick", false) +addEvent("onClientGUIMiddleClick", false) +addEvent("onClientGUIDoubleLeftClick", false) +addEvent("onClientGUIRender", false) + +local _guiSetEnabled = guiSetEnabled +function guiSetEnabled(element, state) + if not scheck("u:element:gui,b") then return false end + if guiGetBooleanProperty(element, "Disabled") == not state then return false end + + local set = _guiSetEnabled(element, state) + if not set then return set end + + triggerEvent(state and "onClientGUIEnabled" or "onClientGUIDisabled", element) + + return true +end + +local _guiSetVisible = guiSetVisible +function guiSetVisible(element, state, ...) + if not scheck("u:element:gui,b") then return false end + + local set = _guiSetVisible(element, state, ...) + if not set then return set end + + triggerEvent(state and "onClientGUIShow" or "onClientGUIHide", element) + + return true +end + +addEventHandler("onClientGUIClick", guiRoot, + function(button, state, absoluteX, absoluteY) + + if button == "left" then + triggerEvent("onClientGUILeftClick", source, absoluteX, absoluteY) + + elseif button == "right" then + triggerEvent("onClientGUIRightClick", source, absoluteX, absoluteY) + + elseif button == "middle" then + triggerEvent("onClientGUIMiddleClick", source, absoluteX, absoluteY) + + end + end +) + +addEventHandler("onClientGUIDoubleClick", guiRoot, + function(button, state, absoluteX, absoluteY) + if button ~= "left" then return end + if state ~= "up" then return end + + triggerEvent("onClientGUIDoubleLeftClick", source, absoluteX, absoluteY) + + end +) + +local render = false +addEventHandler("onClientRender", root, + function() + render = not render + if not render then return end + + for i, element in ipairs(getElementChildren(guiRoot)) do + if guiGetVisible(element) then triggerEvent("onClientGUIRender", element) end + end + + end +) \ No newline at end of file diff --git a/gui/lib/focus.lua b/gui/lib/focus.lua new file mode 100644 index 0000000..a3ad91d --- /dev/null +++ b/gui/lib/focus.lua @@ -0,0 +1,26 @@ + +sourceGUIFocus = nil + +addEventHandler("onClientGUIFocus", guiRoot, + function() + sourceGUIFocus = source + guiSetInternalData(source, "focus", true) + end +) + +addEventHandler("onClientGUIBlur", guiRoot, + function() + guiSetInternalData(source, "focus", false) + end +) + +function guiGetFocused() + + return sourceGUIFocus +end + +function guiGetFocus(element) + if not scheck("u:element:gui") then return false end + + return guiGetInternalData(element, "focus") or false +end \ No newline at end of file diff --git a/gui/lib/font.lua b/gui/lib/font.lua new file mode 100644 index 0000000..3935bfd --- /dev/null +++ b/gui/lib/font.lua @@ -0,0 +1,87 @@ + +local fontSizeAccessLabel = guiCreateLabel(0, 0, 0, 0, "", false) +guiSetVisible(fontSizeAccessLabel, false) +guiSetEnabled(fontSizeAccessLabel, false) +local _guiSetText = guiSetText +local _guiSetFont = guiSetFont + +function guiFontGetHeight(font) + if not scheck("?s|u:element:gui-font") then return false end + + if not font then font = "default-normal" end + + guiSetFont(fontSizeAccessLabel, font) + + return guiLabelGetFontHeight(fontSizeAccessLabel) +end + +function guiFontGetTextExtent(text, font) + if not scheck("s,?s|u:element:gui-font") then return false end + + font = font or "default-normal" + + _guiSetFont(fontSizeAccessLabel, font) + _guiSetText(fontSizeAccessLabel, text) + + return guiLabelGetTextExtent(fontSizeAccessLabel) +end + +function guiFontGetTextSize(text, font, maxWidth) + if not scheck("s,?s|u:element:gui-font,?n") then return false end + + font = font or "default-normal" + + local textExtent = guiFontGetTextExtent(text, font) + local fontHeight = guiFontGetHeight(font) + + if (not maxWidth) or maxWidth >= textExtent then return textExtent, fontHeight end + + local lineCount = 0 + local lineExtent = 0 + local longestLineExtent = 0 + + for il, line in ipairs(utf8.split(text, "\n")) do + + lineCount = lineCount + 1 + lineExtent = 0 + + for iw, word in ipairs(utf8.split(line, "%s")) do + + local wordExtent = guiFontGetTextExtent((iw == 1 and "" or " ")..word, font) + + if lineExtent + wordExtent > maxWidth then + lineCount = lineCount + 1 + lineExtent = guiFontGetTextExtent(word, font) + + else + lineExtent = lineExtent + wordExtent + + end + + if lineExtent > longestLineExtent then + longestLineExtent = lineExtent + end + end + end + + return longestLineExtent, lineCount * fontHeight +end + +function guiFontClipText(text, font, maxWidth) + if not scheck("s,?s|u:element:gui-font,n") then return false end + + font = font or "default-normal" + + local textExtent = guiFontGetTextExtent(text, font) + if maxWidth >= textExtent then return text end + + local clippedText = "" + local clippedExtent = 0 + + for i, symbol in ipairs(utf8.split(text, "")) do + if guiFontGetTextExtent(clippedText..symbol, font) > maxWidth then break end + clippedText = clippedText..symbol + end + + return clippedText +end diff --git a/gui/lib/general.lua b/gui/lib/general.lua new file mode 100644 index 0000000..9b62696 --- /dev/null +++ b/gui/lib/general.lua @@ -0,0 +1,515 @@ + +-------------------------------- +-- VISIBLE +-------------------------------- + +local _guiSetVisible = guiSetVisible +function guiSetVisible(element, state, bringToFront) + if not scheck("u:element:gui,b,?b") then return false end + + local set = _guiSetVisible(element, state) + if set and state and bringToFront then guiBringToFront(element) end + + return set +end + +-------------------------------- +-- POSITIONING +-------------------------------- + +local _guiGetPosition = guiGetPosition +function guiGetPosition(element, relative) + if not scheck("u:element:gui,?b") then return false end + + return _guiGetPosition(element, relative or false) +end + +local _guiSetPosition = guiSetPosition +function guiSetPosition(element, x, y, relative) + if not scheck("u:element:gui,n[2],?b") then return false end + + if not relative then + -- CEGUI BUG, I think + if x < 0 then x = x - 1 end + if y < 0 then y = y - 1 end + end + + return _guiSetPosition(element, x, y, relative or false) +end + +local _guiGetSize = guiGetSize +function guiGetSize(element, relative) + if not scheck("u:element:gui,?b") then return false end + + return _guiGetSize(element, relative or false) +end + +local _guiSetSize = guiSetSize +function guiSetSize(element, width, height, relative) + if not scheck("u:element:gui,n[2],?b") then return false end + + return _guiSetSize(element, width, height, relative or false) +end + +function guiGetXPosition(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local x = guiGetPosition(element, relative or false) + + return x +end + +function guiSetXPosition(element, x, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedXPosition") + value[relative and 1 or 2] = x + value[relative and 2 or 1] = 0 + + return guiSetTableProperty(element, "UnifiedXPosition", value) +end + +function guiGetYPosition(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local _, y = guiGetPosition(element, relative or false) + + return y +end + +function guiSetYPosition(element, y, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedYPosition") + value[relative and 1 or 2] = y + value[relative and 2 or 1] = 0 + + return guiSetTableProperty(element, "UnifiedYPosition", value) +end + +function guiGetWidth(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local width = guiGetSize(element, relative or false) + + return width +end + +function guiSetWidth(element, width, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedWidth") + value[relative and 1 or 2] = width + value[relative and 2 or 1] = 0 + + return guiSetTableProperty(element, "UnifiedWidth", value) +end + +function guiGetHeight(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local _, height = guiGetSize(element, relative or false) + + return height +end + +function guiSetHeight(element, height, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedHeight") + value[relative and 1 or 2] = height + value[relative and 2 or 1] = 0 + + return guiSetTableProperty(element, "UnifiedHeight", value) +end + +function guiSetMaxSize(element, width, height, relative) + if not scheck("u:element:gui,n[2],?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedMaxSize") + + value[1][relative and 1 or 2] = width + value[1][relative and 2 or 1] = 0 + value[2][relative and 1 or 2] = height + value[2][relative and 2 or 1] = 0 + + return guiSetTableProperty(element, "UnifiedMaxSize", value) +end + +function guiSetMinSize(element, width, height, relative) + if not scheck("u:element:gui,n[2],?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedMinSize") + + value[1][relative and 1 or 2] = width + value[1][relative and 2 or 1] = 0 + value[2][relative and 1 or 2] = height + value[2][relative and 2 or 1] = 0 + + return guiSetTableProperty(element, "UnifiedMinSize", value) +end + +function guiSetMaxWidth(element, width, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedMaxSize") + value[1][relative and 1 or 2] = width + value[1][relative and 2 or 1] = 0 + + return guiSetTableProperty(element, "UnifiedMaxSize", value) +end + +function guiSetMaxHeight(element, height, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedMaxSize") + value[2][relative and 1 or 2] = height + value[2][relative and 2 or 1] = 0 + + return guiSetTableProperty(element, "UnifiedMaxSize", value) +end + +-------------------------------- +-- RAW POSITIONING +-------------------------------- + +function guiGetRawPosition(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedPosition") + local x = value[1][relative and 1 or 2] + local y = value[2][relative and 1 or 2] + + return x, y +end + +function guiSetRawPosition(element, x, y, relative) + if not scheck("u:element:gui,n[2],?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedPosition") + value[1][relative and 1 or 2] = x + value[2][relative and 1 or 2] = y + + return guiSetTableProperty(element, "UnifiedPosition", value) +end + +function guiGetRawXPosition(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedXPosition") + + return value[relative and 1 or 2] +end + +function guiSetRawXPosition(element, x, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedXPosition") + value[relative and 1 or 2] = x + + return guiSetTableProperty(element, "UnifiedXPosition", value) +end + +function guiGetRawYPosition(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedYPosition") + + return value[relative and 1 or 2] +end + +function guiSetRawYPosition(element, y, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedYPosition") + value[relative and 1 or 2] = y + + return guiSetTableProperty(element, "UnifiedYPosition", value) +end + +function guiGetRawSize(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedSize") + local width = value[1][relative and 1 or 2] + local height = value[2][relative and 1 or 2] + + return width, height +end + +function guiSetRawSize(element, width, height, relative) + if not scheck("u:element:gui,n[2],?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedSize") + value[1][relative and 1 or 2] = width + value[2][relative and 1 or 2] = height + + return guiSetTableProperty(element, "UnifiedSize", value) +end + +function guiGetRawWidth(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedWidth") + + return value[relative and 1 or 2] +end + +function guiSetRawWidth(element, width, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedWidth") + value[relative and 1 or 2] = width + + return guiSetTableProperty(element, "UnifiedWidth", value) +end + +function guiGetRawHeight(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedHeight") + + return value[relative and 1 or 2] +end + +function guiSetRawHeight(element, height, relative) + if not scheck("u:element:gui,n,?b") then return false end + + local value = guiGetTableProperty(element, "UnifiedHeight") + value[relative and 1 or 2] = height + + return guiSetTableProperty(element, "UnifiedHeight", value) +end + +-------------------------------- +-- GEOMETRY +-------------------------------- + +function guiCenter(element) + if not scheck("u:element:gui") then return false end + + local w, h = guiGetSize(element, false) + local pw, ph = GUI_SCREEN_WIDTH, GUI_SCREEN_HEIGHT + local parent = getElementParent(element) + if parent ~= guiRoot then + pw, ph = guiGetSize(parent, false) + end + + local x, y = (pw-w)/2, (ph-h)/2 + + return guiSetPosition(element, x, y, false) +end + +function guiGetAbsolutePosition(element) + if not scheck("u:element:gui") then return false end + + local x, y = guiGetPosition(element, false) + local parent = getElementParent(element) + while parent ~= guiRoot do + local px, py = guiGetPosition(parent, false) + if getElementType(parent) == "gui-tabpanel" then + y = y + 24 + end + x = x + px + y = y + py + parent = getElementParent(parent) + end + + return x, y +end + +function guiGetCursorPosition(element, relative) + if not scheck("u:element:gui,?b") then return false end + + local ex, ey = guiGetAbsolutePosition(element) + local cx, cy = getCursorAbsolutePosition() + local x, y = cx - ex, cy - ey + if relative then + local w, h = guiGetSize(element, false) + x, y = x/w, y/h + end + + return x, y +end + +function guiIsCursorInside(element) + if not scheck("u:element:gui") then return false end + + local x, y = guiGetAbsolutePosition(element) + local w, h = guiGetSize(element, false) + local cx, cy = getCursorAbsolutePosition() + + return isInsideRectangle(cx, cy, x, y, w, h) +end + +function getCursorAbsolutePosition() + + local cx, cy = getCursorPosition() + return GUI_SCREEN_WIDTH * cx, GUI_SCREEN_HEIGHT * cy +end + +function guiGetAbsoluteFromRelative(x, y, width, height, parent) + if not scheck("n[4],?u:element:gui") then return false end + + local pw, ph = GUI_SCREEN_WIDTH, GUI_SCREEN_HEIGHT + if parent then + pw, ph = guiGetSize(parent, false) + end + + x = x * pw + y = y * ph + width = width * pw + height = height * ph + + return x, y, width, height +end + +-------------------------------- +-- TEXT +-------------------------------- + +local _guiGetFont = guiGetFont +function guiGetFont(element) + if not scheck("u:element:gui") then return false end + + local name = _guiGetFont(element) + return name +end + +function guiGetFontElement(element) + if not scheck("u:element:gui") then return false end + + local _, element = _guiGetFont(element) + return element +end + +function guiGetFontHeight(element) + if not scheck("u:element:gui") then return false end + + return guiFontGetHeight(guiGetFont(element)) +end + +function guiGetTextExtent(element) + if not scheck("u:element:gui") then return false end + + return guiFontGetTextExtent(guiGetText(element), guiGetFont(element)) +end + +function guiGetTextSize(element, maxWidth) + if not scheck("u:element:gui,?n") then return false end + + local width = guiGetSize(element, false) + + return guiFontGetTextSize(guiGetText(element), guiGetFont(element), maxWidth or width) +end + +function guiIsTextClipped(element) + if not scheck("u:element:gui") then return false end + + local w, h = guiGetSize(element, false) + local tw, th = guiGetTextSize(element, w) + + return tw > w or th > h +end + +-------------------------------- +-- ALIGN +-------------------------------- + +function guiSetHorizontalAlign(element, alignment) + if not scheck("u:element:gui,s") then return false end + + return guiSetAlignmentProperty(element, "HorizontalAlignment", alignment) +end + +function guiSetVerticalAlign(element, alignment) + if not scheck("u:element:gui,s") then return false end + + return guiSetAlignmentProperty(element, "VerticalAlignment", alignment) +end + +function guiGetWordWrap(element) + if not scheck("u:element:gui") then return false end + + return utf8.match(guiGetProperty(element, "HorzFormatting"), "^WordWrap") and true or false +end + +-------------------------------- +-- PARENT +-------------------------------- + +function guiSetAlwaysOnTop(element, state) + if not scheck("u:element:gui,b") then return false end + + return guiSetBooleanProperty(element, "AlwaysOnTop", state) +end + +function guiSetClippedByParent(element, state) + if not scheck("u:element:gui,b") then return false end + + return guiSetBooleanProperty(element, "ClippedByParent", state) +end + +function guiSetInheritsAlpha(element, state) + if not scheck("u:element:gui,b") then return false end + + return guiSetBooleanProperty(element, "InheritsAlpha", state) +end + +function guiIsFullyClippedByParent(element) + if not scheck("u:element:gui") then return false end + + local pw, ph = guiGetSize(getElementParent(element), false) + local x, y = guiGetPosition(element, false) + local w, h = guiGetSize(element, false) + + return + x >= pw or + y >= ph or + x + w <= 0 or + y + h <= 0 +end + +function guiGetGreatParent(element) + if not scheck("u:element:gui") then return false end + + local parent = element + while parent ~= guiRoot do + element = parent + parent = getElementParent(element) + end + + return element +end + +-------------------------------- +-- EVENTS +-------------------------------- + +function guiIsGrandParentFor(element, child) + if not scheck("u:element:gui[2]") then return false end + + local parent = child + while parent ~= guiRoot do + if parent == element then return true end + parent = getElementParent(parent) + end + + return false +end + +function guiTriggerEvent(element, event, ...) + if not scheck("u:element:gui,s") then return false end + if not guiGetEnabled(element) then return false end + if not guiGetVisible(element) then return false end + + return triggerEvent(event, element, ...) +end + +function guiClick(element, key) + if not scheck("u:element:gui,?s") then return false end + if not guiGetEnabled(element) then return false end + if guiGetBooleanProperty(element, "Disabled") then return false end + + return guiTriggerEvent(element, "onClientGUIClick", key or "left", "up", guiGetAbsolutePosition(element)) +end \ No newline at end of file diff --git a/gui/lib/gridlist.lua b/gui/lib/gridlist.lua new file mode 100644 index 0000000..6c5d301 --- /dev/null +++ b/gui/lib/gridlist.lua @@ -0,0 +1,278 @@ + +addEvent("onClientGUIGridListItemSelected", false) + +local function onClick() + + local row, column = guiGridListGetSelectedItem(this) + triggerEvent("onClientGUIGridListItemSelected", this, row, column) + +end + +local _guiCreateGridList = guiCreateGridList +function guiCreateGridList(x, y, width, height, relative, parent) + if not scheck("n[4],b,?u:element:gui") then return false end + + local list = _guiCreateGridList(x, y, width, height, relative, parent) + addEventHandler("onClientGUIClick", list, onClick, false) + + return list +end + +local _guiGridListSetVerticalScrollPosition = guiGridListSetVerticalScrollPosition +function guiGridListSetVerticalScrollPosition(list, scroll) + if not scheck("u:element:gui-gridlist,n") then return false end + + setTimer( + function() + if not isElement(list) then return end + _guiGridListSetVerticalScrollPosition(list, scroll) + end, + 0, 1 + ) + + return true +end + +function guiGridListClearColumns(list) + if not scheck("u:element:gui-gridlist") then return false end + + local columns = guiGridListGetColumnCount(list) + if columns == 0 then return false end + + for column = 1, columns do + guiGridListRemoveColumn(list, column) + end + return true +end + +local _guiGridListSetSelectedItem = guiGridListSetSelectedItem +function guiGridListSetSelectedItem(list, row, column) + if not scheck("u:element:gui-gridlist,?n[2]") then return false end + + row = row or 0 + column = column or 1 + + local srow, scolumn = guiGridListGetSelectedItem(list) + if srow == row and scolumn == column then return false end + + if not _guiGridListSetSelectedItem(list, row, column) then return false end + + --guiFocus(list) + guiGridListSetVerticalScrollPosition(list, guiGridListGetVerticalScrollPositionByRow(list, row)) + guiGridListSetHorizontalScrollPosition(list, guiGridListGetHorizontalScrollPositionByColumn(list, column)) + + triggerEvent("onClientGUIGridListItemSelected", list, row, column) + + return true +end + +function guiGridListSetSelectedAnyItem(list, row, column) + if not scheck("u:element:gui-gridlist,n,?n") then return false end + + row = math.min(math.max(row or 0, 0), guiGridListGetRowCount(list) - 1) + column = math.min(math.max(column or 1, 1), guiGridListGetColumnCount(list)) + + return guiGridListSetSelectedItem(list, row, column) +end + +local _guiGridListSetItemText = guiGridListSetItemText +function guiGridListSetItemText(list, row, column, text, isSection, isNumber) + if not scheck("u:element:gui-gridlist,n,n,s,?b[2]") then return false end + + return _guiGridListSetItemText(list, row, column, text, isSection or false, isNumber or false) +end + +local _guiGridListGetItemData = guiGridListGetItemData +function guiGridListGetItemData(list, row, column) + if not scheck("u:element:gui-gridlist,n,?n") then return false end + + return _guiGridListGetItemData(list, row, column or 1) +end + +local _guiGridListGetItemText = guiGridListGetItemText +function guiGridListGetItemText(list, row, column) + if not scheck("u:element:gui-gridlist,n,?n") then return false end + + return _guiGridListGetItemText(list, row, column or 1) +end + +local _guiGridListRemoveRow = guiGridListRemoveRow +function guiGridListRemoveRow(gridList, row) + if not scheck("u:element:gui-gridlist,?n") then return false end + + row = row or (guiGridListGetRowCount(gridList) - 1) + + local srow, scolumn = guiGridListGetSelectedItem(gridList) + if not _guiGridListRemoveRow(gridList, row) then return false end + if srow ~= row then return true end + + local rows = guiGridListGetRowCount(gridList) + if rows == 0 then return true end + + srow = math.clamp(srow, 0, rows - 1) + guiGridListSetSelectedItem(gridList, srow, scolumn) + + return true +end + +function guiGridListSetRowColor(list, row, r, g, b, a) + if not scheck("u:element:gui-gridlist,n[4],?n") then return false end + if row < 0 then return false end + if row > guiGridListGetRowCount(list) - 1 then return false end + + local columns = guiGridListGetColumnCount(list) + if columns == 0 then return false end + + for column = 1, columns do + guiGridListSetItemColor(list, row, column, r, g, b, a or 255) + end + return true +end + +function guiGridListGetRowByText(gridList, text, column) + if not scheck("u:element:gui-gridlist,s,?n") then return false end + + local rows = guiGridListGetRowCount(gridList) + if rows == 0 then return nil end + + local columns = guiGridListGetColumnCount(gridList) + if columns == 0 then return nil end + + column = column or 1 + + for row = 0, rows-1 do + if guiGridListGetItemText(gridList, row, column) == text then return row end + end + return nil +end + +function guiGridListGetItemByData(gridList, data) + if not scheck("u:element:gui-gridlist,!") then return false end + + local rows = guiGridListGetRowCount(gridList) + if rows == 0 then return nil end + + local columns = guiGridListGetColumnCount(gridList) + if columns == 0 then return nil end + + for row = 0, rows-1 do + for column = 1, columns do + if guiGridListGetItemData(gridList, row, column) == data then return row, column end + end + end + return nil +end + +function guiGridListGetSelectedItemText(gridList) + if not scheck("u:element:gui-gridlist") then return false end + + local row, column = guiGridListGetSelectedItem(gridList) + if row < 0 or column < 1 then return nil end + + return guiGridListGetItemText(gridList, row, column) +end + +function guiGridListGetSelectedItemData(gridList, row, column) + if not scheck("u:element:gui-gridlist,?n[2]") then return false end + + local srow, scolumn = guiGridListGetSelectedItem(gridList) + if srow < 0 or scolumn < 1 then return nil end + + return guiGridListGetItemData(gridList, row or srow, column or scolumn) +end + +function guiGridListAdjustHeight(gridList, itemCount) + if not scheck("u:element:gui-gridlist,?n") then return false end + + itemCount = itemCount or guiGridListGetRowCount(gridList) + + return guiSetHeight(gridList, itemCount * 14 + 35 + 15, false) +end + +function guiGridListAdjustColumnWidth(gridList) + if not scheck("u:element:gui-gridlist") then return false end + + local width = guiGetSize(gridList, false) + + return guiGridListSetColumnWidth(gridList, 1, width - 30, false) +end + +local _guiGridListAddColumn = guiGridListAddColumn +function guiGridListAddColumn(gridList, name, width) + if not scheck("u:element:gui-gridlist,s,?n") then return false end + + if width then return _guiGridListAddColumn(gridList, name, width) end + + local add = _guiGridListAddColumn(gridList, name, 1) + if not add then return add end + + guiGridListAdjustColumnWidth(gridList) + + return add +end + +function guiGridListGetVerticalScrollRow(list) + if not scheck("u:element:gui-gridlist") then return false end + + local rows = guiGridListGetRowCount(list) + if rows <= 1 then return 0 end + + return math.floor(rows * (guiGridListGetVerticalScrollPosition(list)/100)) +end + +function guiGridListGetColumnsWidth(list, relative) + if not scheck("u:element:gui-gridlist,b") then return false end + + local columns = guiGridListGetColumnCount(list) + if columns == 0 then return 0 end + + local width = 0 + for column = 1, columns do + width = width + guiGridListGetColumnWidth(list, column, relative) + end + + return width +end + +function guiGridListGetColumnPosition(list, column, relative) + if not scheck("u:element:gui-gridlist,n,b") then return false end + + local columns = guiGridListGetColumnCount(list) + if columns == 0 then return 0 end + if column <= 1 then return 0 end + + local position = 0 + for number = 1, column - 1 do + position = position + guiGridListGetColumnWidth(list, number, relative) + end + + return position +end + +function guiGridListGetVerticalScrollPositionByRow(list, row) + if not scheck("u:element:gui-gridlist,n") then return false end + + if row == -1 then return 0 end + + local rows = guiGridListGetRowCount(list) + if rows <= 1 then return 0 end + + row = math.clamp(row, 0, rows-1) + + return row/(rows-1) * 100 +end + +function guiGridListGetHorizontalScrollPositionByColumn(list, column) + if not scheck("u:element:gui-gridlist,n") then return false end + + if column == 0 then return 0 end + + local columns = guiGridListGetColumnCount(list) + if columns <= 1 then return 0 end + + column = math.clamp(column, 1, columns) + + return + guiGridListGetColumnPosition(list, column, false)/ + guiGridListGetColumnsWidth(list, false) * 100 +end \ No newline at end of file diff --git a/gui/lib/gridlist_focus.lua b/gui/lib/gridlist_focus.lua new file mode 100644 index 0000000..da1a4cd --- /dev/null +++ b/gui/lib/gridlist_focus.lua @@ -0,0 +1,42 @@ + +local GridListFocus = {} + +GridListFocus.focused = nil + +function GridListFocus.onFocus() + + GridListFocus.focused = this + +end + +function GridListFocus.onBlur() + if this ~= GridListFocus.focused then return end + if not guiIsGrandParentFor(source, this) then return end + + GridListFocus.focused = nil + +end + +function GridListFocus.onDestroy() + if this ~= GridListFocus.focused then return end + + GridListFocus.focused = nil + +end + +local _guiCreateGridList = guiCreateGridList +function guiCreateGridList(x, y, width, height, relative, parent) + if not scheck("n[4],b,?u:element:gui") then return false end + + local list = _guiCreateGridList(x, y, width, height, relative, parent) + addEventHandler("onClientGUIFocus", list, GridListFocus.onFocus, false) + addEventHandler("onClientGUIBlur", list, GridListFocus.onBlur) + addEventHandler("onClientElementDestroy", list, GridListFocus.onDestroy, false) + + return list +end + +function guiGetFocusedGridList() + + return GridListFocus.focused +end \ No newline at end of file diff --git a/gui/lib/gridlist_keys.lua b/gui/lib/gridlist_keys.lua new file mode 100644 index 0000000..662cd92 --- /dev/null +++ b/gui/lib/gridlist_keys.lua @@ -0,0 +1,20 @@ + +addEvent("onClientGUIGridListItemDeleteKey", false) +addEvent("onClientGUIGridListItemEnterKey", false) + +addEventHandler("onClientKey", root, + function(key, press) + local list = guiGetFocusedGridList() + if not list then return end + if not press then return end + if not (key == "delete" or key == "enter") then return end + + local row, column = guiGridListGetSelectedItem(list) + if row == -1 or column == 0 then return end + + triggerEvent( + key == "delete" and "onClientGUIGridListItemDeleteKey" or "onClientGUIGridListItemEnterKey", + list, row, column + ) + end +) \ No newline at end of file diff --git a/gui/lib/gridlist_players.lua b/gui/lib/gridlist_players.lua new file mode 100644 index 0000000..fd72c25 --- /dev/null +++ b/gui/lib/gridlist_players.lua @@ -0,0 +1,85 @@ + +local gridPlayersList = {} + +gridPlayersList.lists = {} + +function gridPlayersList.refresh(list) + + local selected = guiGridListGetSelectedItemData(list) + local selectedRow = guiGridListGetSelectedItem(list) + + guiGridListClear(list) + for i, player in ipairs(getElementsByType("player")) do + local row = guiGridListAddRow(list, getPlayerName(player, true)) + guiGridListSetItemData(list, row, 1, player) + end + + if guiGridListGetAutoSearchEdit(list) then guiGridListAutoSearchEditReloadItems(list) end + + selectedRow = selected and guiGridListGetItemByData(list, selected) or selectedRow + guiGridListSetSelectedAnyItem(list, selectedRow) + + return true +end + +function gridPlayersList.onJoin() + + for i, list in ipairs(gridPlayersList.lists) do + local row = guiGridListAddRow(list, getPlayerName(source, true)) + guiGridListSetItemData(list, row, 1, source) + if guiGridListGetAutoSearchEdit(list) then guiGridListAutoSearchEditReloadItems(list) end + end + +end + +function gridPlayersList.onQuit() + + for i, list in ipairs(gridPlayersList.lists) do + local row = guiGridListGetItemByData(list, source) + if row then + guiGridListRemoveRow(list, row) + if guiGridListGetAutoSearchEdit(list) then guiGridListAutoSearchEditReloadItems(list) end + end + end + +end + +function gridPlayersList.onChangeNick() + + for i, list in ipairs(gridPlayersList.lists) do + local row = guiGridListGetItemByData(list, source) + if row then + guiGridListSetItemText(list, row, 1, getPlayerName(source, true)) + if guiGridListGetAutoSearchEdit(list) then guiGridListAutoSearchEditReloadItems(list) end + end + end + +end + +function gridPlayersList.onDestroy() + if source ~= this then return end + + table.removevalue(gridPlayersList.lists, this) + + return true +end + +addEventHandler("onClientPlayerJoin", root, gridPlayersList.onJoin) +addEventHandler("onClientPlayerQuit", root, gridPlayersList.onQuit) +addEventHandler("onClientPlayerChangeNick", root, gridPlayersList.onChangeNick) + +function guiGridListSetContentPlayers(list) + if not scheck("u:element:gui-gridlist") then return false end + if table.index(gridPlayersList.lists, list) then return false end + + guiGridListClear(list) + guiGridListClearColumns(list) + guiGridListAddColumn(list, "") + + table.insert(gridPlayersList.lists, list) + gridPlayersList.refresh(list) + + addEventHandler("onClientElementDestroy", list, gridPlayersList.onDestroy) + + return true +end \ No newline at end of file diff --git a/gui/lib/gridlist_scroll.lua b/gui/lib/gridlist_scroll.lua new file mode 100644 index 0000000..f909bdf --- /dev/null +++ b/gui/lib/gridlist_scroll.lua @@ -0,0 +1,52 @@ + +local ARROW_PRESSED_DELAY = 500 + +local arrowPressedTicks = nil +local arrowHorizontal = false +local pressedArrowVector = 0 + +local GridListScroll = {} + +function GridListScroll.scroll() + + local list = guiGetFocusedGridList() + if not list then return false end + + local rows = guiGridListGetRowCount(list) + if rows == 0 then return end + + local columns = guiGridListGetColumnCount(list) + if columns == 0 then return end + + local row, column = guiGridListGetSelectedItem(list) + if arrowHorizontal then + column = math.clamp(column + pressedArrowVector, 1, columns) + else + row = math.clamp(row + pressedArrowVector, 0, rows - 1) + end + guiGridListSetSelectedItem(list, row, column) + +end + +addEventHandler("onClientKey", root, + function(key, press) + if not (key == "arrow_d" or key == "arrow_u" or key == "arrow_l" or key == "arrow_r") then return end + if not press then arrowPressedTicks = nil return end + + local list = guiGetFocusedGridList() + if not list then return end + if not guiGetVisible(list) or not guiGetEnabled(list) then return end + + pressedArrowVector = (key == "arrow_d" or key == "arrow_r") and 1 or -1 + arrowHorizontal = key == "arrow_l" or key == "arrow_r" + arrowPressedTicks = getTickCount() + GridListScroll.scroll() + end +) + +addEventHandler("onClientRender", root, + function() + if not arrowPressedTicks then return end + if getTickCount() - arrowPressedTicks > ARROW_PRESSED_DELAY then GridListScroll.scroll() end + end +) \ No newline at end of file diff --git a/gui/lib/gridlist_search.lua b/gui/lib/gridlist_search.lua new file mode 100644 index 0000000..3e08fc2 --- /dev/null +++ b/gui/lib/gridlist_search.lua @@ -0,0 +1,130 @@ + +local GridListSearch = {} + +function GridListSearch.getRowData(list, row) + + local data = {} + for column = 1, guiGridListGetColumnCount(list) do + local itemData = { + text = guiGridListGetItemText(list, row, column), + data = guiGridListGetItemData(list, row, column) + } + table.insert(data, itemData) + end + return data +end + +function GridListSearch.getItemsData(list) + + local data = {} + for row = 0, guiGridListGetRowCount(list) - 1 do + table.insert(data, GridListSearch.getRowData(list, row)) + end + return data +end + +function GridListSearch.onChanged(edit) + + local list = guiGetInternalData(edit, "search.list") + local selected = guiGridListGetSelectedItem(list) + if selected == -1 then selected = 0 end + + local selectedData = GridListSearch.getRowData(list, selected) + guiGridListClear(list) + + local text = utf8.trim(guiGetText(edit)) + if text == "" then text = false end + + for _, rowData in ipairs(guiGetInternalData(list, "search.items")) do + local found = false + for column, data in ipairs(rowData) do + if (not text) or utf8.find(data.text, text, nil, true, true) then + found = true + break + end + end + if found then + local row = guiGridListAddRow(list) + for column, data in ipairs(rowData) do + guiGridListSetItemText(list, row, column, data.text, false, false) + guiGridListSetItemData(list, row, column, data.data) + end + end + end + + if guiGridListGetRowCount(list) == 0 then + local row = guiGridListAddRow(list) + for column, data in ipairs(selectedData) do + guiGridListSetItemText(list, row, column, data.text, false, false) + guiGridListSetItemData(list, row, column, data.data) + end + end + guiGridListSetSelectedItem(list, 0, 1) + +end + +function GridListSearch.onListDestroy() + if source ~= this then return end + + GridListSearch.reset(this) + +end + +function GridListSearch.onEditDestroy() + if source ~= this then return end + + GridListSearch.reset(guiGetInternalData(this, "search.list")) + +end + +function GridListSearch.reset(list) + + local edit = guiGetInternalData(list, "search.edit") + if edit and isElement(edit) then + removeEventHandler("onClientGUIChanged", edit, GridListSearch.onChanged) + removeEventHandler("onClientElementDestroy", edit, GridListSearch.onEditDestroy) + guiSetInternalData(edit, "search.list", nil) + end + + if isElement(list) then + removeEventHandler("onClientElementDestroy", list, GridListSearch.onListDestroy) + guiSetInternalData(list, "search.edit", nil) + guiSetInternalData(list, "search.items", nil) + end + + return true +end + +function guiGridListSetAutoSearchEdit(list, edit) + if not scheck("u:element:gui-gridlist,?u:element:gui-edit") then return false end + if guiGetInternalData(list, "search.edit") == edit then return false end + + GridListSearch.reset(list) + + guiSetInternalData(list, "search.items", GridListSearch.getItemsData(list)) + guiSetInternalData(list, "search.edit", edit) + guiSetInternalData(edit, "search.list", list) + + addEventHandler("onClientGUIChanged", edit, GridListSearch.onChanged, false) + addEventHandler("onClientElementDestroy", list, GridListSearch.onListDestroy, false) + addEventHandler("onClientElementDestroy", edit, GridListSearch.onEditDestroy, false) + GridListSearch.onChanged(edit) + + return true +end + +function guiGridListGetAutoSearchEdit(list) + if not scheck("u:element:gui-gridlist") then return false end + + return guiGetInternalData(list, "search.edit") +end + +function guiGridListAutoSearchEditReloadItems(list) + if not scheck("u:element:gui-gridlist") then return false end + if not guiGetInternalData(list, "search.edit") then return false end + + guiSetInternalData(list, "search.items", GridListSearch.getItemsData(list)) + GridListSearch.onChanged(guiGetInternalData(list, "search.edit")) + + return true +end \ No newline at end of file diff --git a/gui/lib/image.lua b/gui/lib/image.lua new file mode 100644 index 0000000..0e01511 --- /dev/null +++ b/gui/lib/image.lua @@ -0,0 +1,69 @@ + +-- NEEDED +local _guiStaticImageLoadImage = guiStaticImageLoadImage +function guiStaticImageLoadImage(image, path) + if not scheck("u:element:gui-staticimage,s") then return false end + + local propagation = isElementCallPropagationEnabled(image) + setElementCallPropagationEnabled(image, false) + + local result = _guiStaticImageLoadImage(image, path) + + setElementCallPropagationEnabled(image, propagation) + + return result +end + +function guiStaticImageGetColor(image) + if not scheck("u:element:gui-staticimage") then return false end + + return guiGetColorsProperty(image, "ImageColours") +end + +function guiStaticImageSetColor(image, r, g, b, a) + if not scheck("u:element:gui-staticimage,n[3],?n") then return false end + + return guiSetColorsProperty(image, "ImageColours", r, g, b, a) +end + +function guiStaticImageSetSizeNative(image) + if not scheck("u:element:gui-staticimage") then return false end + + local w, h = guiStaticImageGetNativeSize(image) + guiSetMaxSize(image, w, h, false) + + return guiSetSize(image, w, h, false) +end + +function guiStaticImageSetStretched(image, state) + if not scheck("u:element:gui-staticimage,b") then return false end + + guiSetProperty(image, "HorzFormatting", state and "HorzStretched" or "LeftAligned") + guiSetProperty(image, "VertFormatting", state and "VertStretched" or "TopAligned") + + return true +end + +function guiStaticImageScale(image, maxw, maxh) + if not scheck("u:element:gui-staticimage,n[2]") then return false end + + local w, h = guiStaticImageGetNativeSize(image) + if w > maxw then + h = h * (maxw/w) + w = maxw + end + if h > maxh then + w = w * (maxh/h) + h = maxh + end + + return guiSetSize(image, w, h, false) +end + +function guiStaticImageAdjustMaxSize(image) + if not scheck("u:element:gui-staticimage") then return false end + + local nw, nh = guiStaticImageGetNativeSize(image) + + return guiSetMaxSize(image, nw, nh, false) +end \ No newline at end of file diff --git a/gui/lib/input.lua b/gui/lib/input.lua new file mode 100644 index 0000000..0ae2b18 --- /dev/null +++ b/gui/lib/input.lua @@ -0,0 +1,50 @@ + +guiSetInputMode("no_binds_when_editing") + +sourceGUIInput = nil + +local Focus = {} + +function Focus.onFocus() + + sourceGUIInput = source + +end + +function Focus.onBlur() + if sourceGUIInput ~= this then return end + + sourceGUIInput = nil + +end + +local _guiCreateEdit = guiCreateEdit +function guiCreateEdit(x, y, w, h, text, relative, parent) + if not scheck("n[4],s,b,?u:element:gui") then return false end + + local edit = _guiCreateEdit(x, y, w, h, text, relative, parent) + if not edit then return edit end + + addEventHandler("onClientGUIFocus", edit, Focus.onFocus, false) + addEventHandler("onClientGUIBlur", edit, Focus.onBlur) + + return edit +end + +local _guiCreateMemo = guiCreateMemo +function guiCreateMemo(x, y, w, h, text, relative, parent) + if not scheck("n[4],s,b,?u:element:gui") then return false end + + local memo = _guiCreateMemo(x, y, w, h, text, relative, parent) + if not memo then return memo end + + addEventHandler("onClientGUIFocus", memo, Focus.onFocus, false) + addEventHandler("onClientGUIBlur", memo, Focus.onBlur) + + return memo +end + +function guiGetFocusedInput() + + return sourceGUIInput +end \ No newline at end of file diff --git a/gui/lib/input_matcher.lua b/gui/lib/input_matcher.lua new file mode 100644 index 0000000..f4d0737 --- /dev/null +++ b/gui/lib/input_matcher.lua @@ -0,0 +1,35 @@ + +local NUMERIC_MATCHER_STRING = "%-?%d*%.?%d?%d?%d?%d?%d?%d?" +local NUMERIC_POSITIVE_MATCHER_STRING = "%d*%.?%d?%d?%d?%d?%d?%d?" +local NUMERIC_INTEGER_MATCHER_STRING = "%-?%d*" +local NUMERIC_INTEGER_POSITIVE_MATCHER_STRING = "%d*" + +local InputMatcher = {} + +function InputMatcher.getNumericMatcher(integerOnly, min, max, default) + + return + function(text, hard) + + local matcherString = + integerOnly and (min and min >= 0 and NUMERIC_INTEGER_POSITIVE_MATCHER_STRING or NUMERIC_INTEGER_MATCHER_STRING) or + (min and min >= 0 and NUMERIC_POSITIVE_MATCHER_STRING or NUMERIC_MATCHER_STRING) + text = utf8.match(text, matcherString) + text = text or "" + + if not hard then return text end + + local number = tonumber(text) + if not number then return default or "" end + + number = math.clamp(number, min, max) + + return tostring(number) + end +end + +function guiGetInputNumericMatcher(integerOnly, min, max, default) + if not scheck("?b,?n[3]") then return false end + + return InputMatcher.getNumericMatcher(integerOnly, min, max, default) +end \ No newline at end of file diff --git a/gui/lib/input_parser.lua b/gui/lib/input_parser.lua new file mode 100644 index 0000000..d9ab4b0 --- /dev/null +++ b/gui/lib/input_parser.lua @@ -0,0 +1,69 @@ + +local NUMERIC_ROUND = 6 + +local InputParser = {} + +function InputParser.parseNotEmpty(text) + + local value = utf8.trim(text) + if value == "" then return nil end + + return value +end + +function InputParser.parseSearch(text) + + local value = utf8.lower(utf8.trim(text)) + if value == "" then return nil end + + return value +end + +function InputParser.parseMap(text) + + text = utf8.trim(text) + local map = {} + for k, v in utf8.gmatch(text, "([^%s]-):([^%s]*)") do + map[k] = v ~= "" and v or false + end + return map +end + +function InputParser.getNumericParser(integerOnly, min, max) + + return + function(text) + + local value = utf8.trim(text) + if value == "" then return nil end + + value = tonumber(value) + if not value then return nil end + + value = integerOnly and math.floor(value) or math.round(value, 6) + value = math.clamp(value, min, max) + + return value + end +end + +function guiGetInputNotEmptyParser() + + return InputParser.parseNotEmpty +end + +function guiGetInputSearchParser() + + return InputParser.parseSearch +end + +function guiGetInputMapParser() + + return InputParser.parseMap +end + +function guiGetInputNumericParser(integerOnly, min, max) + if not scheck("?b,?n[2]") then return false end + + return InputParser.getNumericParser(integerOnly, min, max) +end \ No newline at end of file diff --git a/gui/lib/internaldata.lua b/gui/lib/internaldata.lua new file mode 100644 index 0000000..bd845f3 --- /dev/null +++ b/gui/lib/internaldata.lua @@ -0,0 +1,26 @@ + +local data = {} + +function guiSetInternalData(element, key, value) + if not scheck("u:element:gui,s") then return false end + + if not data[element] then data[element] = {} end + data[element][key] = value + + return true +end + +function guiGetInternalData(element, key) + if not scheck("u:element:gui,s") then return false end + + if not data[element] then return nil end + return data[element][key] +end + +addEventHandler("onClientElementDestroy", guiRoot, + function() + data[source] = nil + end, + true, + "low" +) \ No newline at end of file diff --git a/gui/lib/label.lua b/gui/lib/label.lua new file mode 100644 index 0000000..3132615 --- /dev/null +++ b/gui/lib/label.lua @@ -0,0 +1,148 @@ + +local _guiLabelGetFontHeight = guiLabelGetFontHeight +function guiLabelGetFontHeight(label) + if not scheck("u:element:gui-label") then return false end + + return _guiLabelGetFontHeight(label) - 1 +end + +function guiLabelGetColor(label) + if not scheck("u:element:gui-label") then return false end + + return guiGetColorsProperty(label, "TextColours") +end + +function guiLabelSetColor(label, r, g, b, a) + if not scheck("u:element:gui-label,byte[3],?byte") then return false end + + return guiSetColorsProperty(label, "TextColours", r, g, b, a) +end + +local _guiLabelGetColor = guiLabelGetColor +function guiLabelGetColor(label) + if not scheck("u:element:gui-label") then return false end + + local normal = guiGetCustomProperty(label, "NormalTextColor") + if normal then return table.unpack(normal) end + + return _guiLabelGetColor(label) +end + +local _guiLabelSetColor = guiLabelSetColor +function guiLabelSetColor(label, r, g, b, a) + if not scheck("u:element:gui-label,byte[3],?byte") then return false end + + guiSetCustomProperty(label, "NormalTextColor", {r, g, b, a}) + return _guiLabelSetColor(label, r, g, b, a) +end + +function guiLabelSetHoverColor(label, r, g, b, a) + if not scheck("u:element:gui-label,?byte[4]") then return false end + + if not guiGetCustomProperty(label, "NormalTextColor") then + guiSetCustomProperty(label, "NormalTextColor", {_guiLabelGetColor(label)}) + end + + if not r then return guiSetCustomProperty(label, "HoverTextColor", nil) end + return guiSetCustomProperty(label, "HoverTextColor", {r, g, b, a}) +end + +function guiLabelSetDisabledColor(label, r, g, b, a) + if not scheck("u:element:gui-label,?byte[4]") then return false end + + if not guiGetCustomProperty(label, "NormalTextColor") then + guiSetCustomProperty(label, "NormalTextColor", {_guiLabelGetColor(label)}) + end + + if not r then return guiSetCustomProperty(label, "DisabledTextColor", nil) end + return guiSetCustomProperty(label, "DisabledTextColor", {r, g, b, a}) +end + +function guiLabelSetHoverFont(label, font) + if not scheck("u:element:gui-label,?s|u:element:gui-font") then return false end + + if not font then return guiSetCustomProperty(label, "HoverTextFont", nil) end + + if not guiGetCustomProperty(label, "NormalTextFont") then + guiSetCustomProperty(label, "NormalTextFont", guiGetFont(label)) + end + + return guiSetCustomProperty(label, "HoverTextFont", font) +end + +function guiLabelAdjustWidth(label) + if not scheck("u:element:gui-label") then return false end + + local width = guiGetTextExtent(label) + + return guiSetWidth(label, width, false) +end + +function guiLabelAdjustHeight(label) + if not scheck("u:element:gui-label") then return false end + + local _, height = guiGetTextSize(label) + + return guiSetHeight(label, height, false) +end + +function guiLabelAdjustSize(label) + if not scheck("u:element:gui-label") then return false end + + local w, h = guiFontGetTextSize(guiGetText(label), guiGetFont(label)) + return guiSetSize(label, w, h, false) +end + +local Label = {} + +function Label.onMouseEnter() + + local color = guiGetCustomProperty(this, "HoverTextColor") + if color then _guiLabelSetColor(this, table.unpack(color)) end + + local font = guiGetCustomProperty(this, "HoverTextFont") + if font then guiSetFont(this, font) end + +end + +function Label.onMouseLeave() + + local color = guiGetCustomProperty(this, "NormalTextColor") + if color then _guiLabelSetColor(this, table.unpack(color)) end + + local font = guiGetCustomProperty(this, "NormalTextFont") + if font then guiSetFont(this, font) end + +end + +function Label.onDisabled() + if not guiIsGrandParentFor(source, this) then return end + + local color = guiGetCustomProperty(this, "DisabledTextColor") + if color then _guiLabelSetColor(this, table.unpack(color)) end + +end + +function Label.onEnabled() + if not guiIsGrandParentFor(source, this) then return end + if not guiGetEnabled(this) then return end + + local color = guiGetCustomProperty(this, "NormalTextColor") + if color then _guiLabelSetColor(this, table.unpack(color)) end + +end + +local _guiCreateLabel = guiCreateLabel +function guiCreateLabel(x, y, width, height, text, relative, parent) + if not scheck("n[4],s,b,?u:element:gui") then return false end + + local label = _guiCreateLabel(x, y, width, height, text, relative, parent) + if not label then return label end + + addEventHandler("onClientMouseEnter", label, Label.onMouseEnter, false) + addEventHandler("onClientMouseLeave", label, Label.onMouseLeave, false) + addEventHandler("onClientGUIEnabled", label, Label.onEnabled) + addEventHandler("onClientGUIDisabled", label, Label.onDisabled) + + return label +end \ No newline at end of file diff --git a/gui/lib/memo.lua b/gui/lib/memo.lua new file mode 100644 index 0000000..78bcff3 --- /dev/null +++ b/gui/lib/memo.lua @@ -0,0 +1,40 @@ + +local Memo = {} + +function Memo.onChanged() + + if utf8.sub(guiGetText(this), -2, -2) ~= "\n" then return end + + triggerEvent("onClientGUIAccepted", this, this) + +end + +local _guiCreateMemo = guiCreateMemo +function guiCreateMemo(x, y, width, height, text, relative, parent) + if not scheck("n[4],s,b,?u:element:gui") then return false end + + local memo = _guiCreateMemo(x, y, width, height, text, relative, parent) + if not memo then return memo end + + addEventHandler("onClientGUIChanged", memo, Memo.onChanged, false) + + return memo +end + +function guiMemoSetWordWrap(memo, state) + if not scheck("u:element:gui-memo,b") then return false end + + return guiSetBooleanProperty(memo, "WordWrap", state) +end + +function guiMemoGetCaratIndex(memo) + if not scheck("u:element:gui-memo") then return false end + + return guiGetNumericProperty(memo, "CaratIndex") +end + +function guiMemoSetCaratIndex(memo, index) + if not scheck("u:element:gui-memo,n") then return false end + + return guiSetNumericProperty(memo, "CaratIndex", index) +end \ No newline at end of file diff --git a/gui/lib/memo_parser.lua b/gui/lib/memo_parser.lua new file mode 100644 index 0000000..729da05 --- /dev/null +++ b/gui/lib/memo_parser.lua @@ -0,0 +1,22 @@ + +function guiMemoSetParser(memo, parser) + if not scheck("u:element:gui-memo,?f") then return false end + + return guiSetInternalData(memo, "parser", parser) +end + +function guiMemoGetParser(memo) + if not scheck("u:element:gui-memo") then return false end + + return guiGetInternalData(memo, "parser") +end + +function guiMemoGetParsedValue(memo) + if not scheck("u:element:gui-memo") then return false end + + local text = guiGetText(memo) + local parser = guiGetInternalData(memo, "parser") + if not parser then return text end + + return parser(text) +end \ No newline at end of file diff --git a/gui/lib/properties.lua b/gui/lib/properties.lua new file mode 100644 index 0000000..c9de837 --- /dev/null +++ b/gui/lib/properties.lua @@ -0,0 +1,119 @@ + +function guiToCEGUIColor(r, g, b, a) + if not scheck("n[3],?n") then return false end + + return string.format("%.2X%.2X%.2X%.2X", a or 255, r, g, b) +end + +function guiFromCEGUIColor(hex) + if not scheck("s") then return false end + + local a = tonumber("0x"..utf8.sub(hex, 1, 2)) + local r, g, b = hexs2rgb("#"..utf8.sub(hex, 3, 8)) + + return r, g, b, a +end + +function guiSetBooleanProperty(element, property, state) + if not scheck("u:element:gui,s,b") then return false end + + return guiSetProperty(element, property, state and "True" or "False") +end + +function guiGetBooleanProperty(element, property) + if not scheck("u:element:gui,s") then return false end + + return guiGetProperty(element, property) == "True" +end + +function guiSetNumericProperty(element, property, value) + if not scheck("u:element:gui,s,n") then return false end + + return guiSetProperty(element, property, tostring(value)) +end + +function guiGetNumericProperty(element, property) + if not scheck("u:element:gui,s") then return false end + + return tonumber(guiGetProperty(element, property)) +end + +function guiSetTableProperty(element, property, value) + if not scheck("u:element:gui,s,t") then return false end + + value = toJSON(value) + :gsub("^%[", "") + :gsub("%]$", "") + :gsub("%[", "{") + :gsub("%]", "}") + :gsub("%s", "") + + return guiSetProperty(element, property, value) +end + +function guiGetTableProperty(element, property) + if not scheck("u:element:gui,s") then return false end + + local value = guiGetProperty(element, property) + :gsub("{", "[") + :gsub("}","]") + + value = fromJSON("["..value.."]") + + return value +end + +function guiSetColorProperty(element, property, r, g, b, a) + if not scheck("u:element:gui,s,n[3],?n") then return false end + + return guiSetProperty(element, property, guiToCEGUIColor(r, g, b, a)) +end + +function guiGetColorProperty(element, property) + if not scheck("u:element:gui,s") then return false end + + return guiFromCEGUIColor(guiGetProperty(element, property)) +end + +function guiSetColorsProperty(element, property, r, g, b, a) + if not scheck("u:element:gui,s,n[3],?n") then return false end + + if not a then a = 255 end + local hex = guiToCEGUIColor(r, g, b, a) + + return guiSetProperty(element, property, "tl:"..hex.." tr:"..hex.." bl:"..hex.." br:"..hex) +end + +function guiGetColorsProperty(element, property) + if not scheck("u:element:gui,s") then return false end + + local property = guiGetProperty(element, property) + local hex = utf8.match(property, "tl:(%x+)") + + return guiFromCEGUIColor(hex) +end + +local ceguiAlignmentProperties = { + ["top"] = "Top", + ["center"] = "Centre", + ["bottom"] = "Bottom", + ["left"] = "Left", + ["right"] = "Right" +} + +function guiSetAlignmentProperty(element, property, alignment) + if not scheck("u:element:gui,s[2]") then return false end + if not ceguiAlignmentProperties[alignment] then return false end + + return guiSetProperty(element, property, ceguiAlignmentProperties[alignment]) +end + +function guiGetAlignmentProperty(element, property) + if not scheck("u:element:gui,s") then return false end + + local value = guiGetProperty(element, property) + local alignment = table.key(ceguiAlignmentProperties, property) + if not alignment then return false end + + return alignment +end \ No newline at end of file diff --git a/gui/lib/scrollpane.lua b/gui/lib/scrollpane.lua new file mode 100644 index 0000000..8665dea --- /dev/null +++ b/gui/lib/scrollpane.lua @@ -0,0 +1,45 @@ + +local DEFAULT_VERTICAL_STEP_SIZE = 0.05 +local SCROLL_SPEED = 5 + +addEvent("onClientGUIScrollPaneVerticalScroll", false) + +local ScrollPane = {} + +function ScrollPane.onRender() + if not guiGetEnabled(this) then return end + + local oldPosition = guiGetInternalData(this, "verticalScroll") + local newPosition = guiScrollPaneGetVerticalScrollPosition(this) + if oldPosition == newPosition then return false end + + guiSetInternalData(this, "verticalScroll", newPosition) + triggerEvent("onClientGUIScrollPaneVerticalScroll", this) + +end + +function ScrollPane.onMouseWheel(vector) + + if this == source then return end + if not guiIsGrandParentFor(this, source) then return end + + local height = guiGetHeight(this, false) + local position = (guiScrollPaneGetVerticalScrollPosition(this)/100 * height - SCROLL_SPEED * vector)/height * 100 + guiScrollPaneSetVerticalScrollPosition(this, position) + +end + +local _guiCreateScrollPane = guiCreateScrollPane +function guiCreateScrollPane(x, y, width, height, relative, parent) + if not scheck("n[4],b,?u:element:gui") then return false end + + local pane = _guiCreateScrollPane(x, y, width, height, relative, parent) + if not pane then return pane end + + guiSetProperty(pane, "VertStepSize", tostring(DEFAULT_VERTICAL_STEP_SIZE)) + + addEventHandler("onClientMouseWheel", pane, ScrollPane.onMouseWheel) + addEventHandler("onClientGUIRender", pane, ScrollPane.onRender) + + return pane +end \ No newline at end of file diff --git a/gui/lib/status.lua b/gui/lib/status.lua new file mode 100644 index 0000000..e78099d --- /dev/null +++ b/gui/lib/status.lua @@ -0,0 +1,44 @@ + +local StatusText = {} + +StatusText.labels = {} + +function StatusText.init(element) + + local label = guiCreateLabel(0, 0, 1, 1, "", true, element) + guiSetEnabled(label, false) + guiSetVisible(label, false) + guiLabelSetHorizontalAlign(label, "center") + guiLabelSetVerticalAlign(label, "center") + + StatusText.labels[element] = label + + return true +end + +function StatusText.set(element, text, r, g, b) + + local label = StatusText.labels[element] + if not label then StatusText.init(element) end + + label = StatusText.labels[element] + guiSetText(label, text) + guiLabelSetColor(label, r, g, b) + + return true +end + +function guiSetStatusText(element, text, r, g, b) + if not scheck("u:element:gui,?s,?byte[3]") then return false end + + if not text then text = "" end + if not r then + if getElementType(element) == "gui-memo" then + r, g, b = 0, 0, 0 + else + r, g, b = 255, 255, 255 + end + end + + return StatusText.set(element, text, r, g, b) +end \ No newline at end of file diff --git a/gui/lib/temptext.lua b/gui/lib/temptext.lua new file mode 100644 index 0000000..6a32cd6 --- /dev/null +++ b/gui/lib/temptext.lua @@ -0,0 +1,33 @@ + +local DEFAULT_INTERVAL = 1 * 10^3 + +local TempText = {} + +function TempText.reset(element, text) + if not isElement(element) then return end + if guiGetText(element) ~= text then return end + + guiSetText(element, guiGetInternalData(element, "tempText.original")) + guiSetInternalData(element, "tempText.original", nil) + guiSetInternalData(element, "tempText.timer", nil) + +end + +function guiSetTempText(element, text, interval) + if not scheck("u:element:gui,s,?n") then return false end + if interval and interval < 1 then return warn("invalid interval", 2) and false end + + if not text then text = "" end + if not interval then interval = DEFAULT_INTERVAL end + + guiSetInternalData(element, "tempText.original", guiGetInternalData(element, "tempText.original") or guiGetText(element)) + guiSetText(element, text) + + local timer = guiGetInternalData(element, "tempText.timer") + if timer and isTimer(timer) then killTimer(timer) end + + timer = setTimer(TempText.reset, interval, 1, element, text) + guiSetInternalData(element, "tempText.timer", timer) + + return true +end \ No newline at end of file diff --git a/gui/lib/widgets/colorpicker.lua b/gui/lib/widgets/colorpicker.lua new file mode 100644 index 0000000..cc38b7c --- /dev/null +++ b/gui/lib/widgets/colorpicker.lua @@ -0,0 +1,376 @@ + +addEvent("onClientGUIColorPickerAccepted", false) + +local gs = { + bg = {0, 0, 0, 255}, + alpha = 0.8, + mrg = 10, + mrg2 = 5, + btn = {w = 80, h = 20}, + edit = {w = 50}, + lbl = {w = guiFontGetTextExtent("H:")}, + bar = {w = 128} +} + +local ColorPicker = {} + +ColorPicker.elements = {} +ColorPicker.thread = nil +ColorPicker.accepted = false +ColorPicker.gui = {} +ColorPicker.color = { + r = 0, g = 0, b = 0, + h = 0, s = 0, v = 0 +} + +function ColorPicker.init() + + ColorPicker.gui.window = guiCreateStaticImage(0, 0, 0, 0, GUI_WHITE_IMAGE_PATH, false) + guiStaticImageSetColor(ColorPicker.gui.window, table.unpack(gs.bg)) + guiSetAlpha(ColorPicker.gui.window, gs.alpha) + guiSetEnabled(ColorPicker.gui.window, false) + guiSetVisible(ColorPicker.gui.window, false) + + local mainHlo = guiCreateHorizontalLayout(gs.mrg, gs.mrg, 0, 0, gs.mrg, false, ColorPicker.gui.window) + + ColorPicker.gui.colors = {} + + local btnsVlo = guiCreateVerticalLayout(0, 0, 0, 0, gs.mrg2, false, mainHlo) + local container = guiCreateContainer(0, 0, gs.btn.w, gs.btn.h, false, btnsVlo) + ColorPicker.gui.currentImg = guiCreateStaticImage(0, 0, 1, 1, GUI_WHITE_IMAGE_PATH, true, container) + guiStaticImageSetColor(ColorPicker.gui.currentImg, 255, 0, 0) + guiSetProperty(ColorPicker.gui.currentImg, "InheritsAlpha", "False") + + ColorPicker.gui.colors.hex = {} + ColorPicker.gui.colors.hex.lbl = guiCreateLabel(0, 0, 1, 1, "", true, ColorPicker.gui.currentImg) + guiSetEnabled(ColorPicker.gui.colors.hex.lbl, false) + guiLabelSetVerticalAlign(ColorPicker.gui.colors.hex.lbl, "center") + guiLabelSetHorizontalAlign(ColorPicker.gui.colors.hex.lbl, "center") + + ColorPicker.gui.colors.hex.edit = guiCreateEdit(0, 0, 1, 1, "", true, container) + guiSetProperty(ColorPicker.gui.colors.hex.edit, "ValidationString", "[0-9A-Fa-f]*") + guiSetVisible(ColorPicker.gui.colors.hex.edit, false) + guiSetInternalData(ColorPicker.gui.colors.hex.edit, "type", "hex") + addEventHandler("onClientGUIChanged", ColorPicker.gui.colors.hex.edit, ColorPicker.onHexEditChanged, false) + + ColorPicker.gui.ok = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Ok", false, btnsVlo) + ColorPicker.gui.cancel = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Cancel", false, btnsVlo) + + local rgbVlo = guiCreateVerticalLayout(0, 0, 0, 0, gs.mrg2, false, mainHlo) + for i, clr in ipairs({"r", "g", "b"}) do + + ColorPicker.gui.colors[clr] = {} + local hlo = guiCreateHorizontalLayout(0, 0, 0, 0, 0, false, rgbVlo) + + ColorPicker.gui.colors[clr].lbl = guiCreateLabel(0, 0, gs.lbl.w, gs.btn.h, utf8.upper(clr)..":", false, hlo) + guiLabelSetVerticalAlign(ColorPicker.gui.colors[clr].lbl, "center") + + ColorPicker.gui.colors[clr].edit = guiCreateEdit(0, 0, gs.edit.w, gs.btn.h, "0", false, hlo) + guiEditSetMatcher(ColorPicker.gui.colors[clr].edit, guiGetInputNumericMatcher(true, 0, 255, 0)) + guiEditSetParser(ColorPicker.gui.colors[clr].edit, guiGetInputNumericParser(true, 0, 255)) + guiSetInternalData(ColorPicker.gui.colors[clr].edit, "type", clr) + addEventHandler("onClientGUIChanged", ColorPicker.gui.colors[clr].edit, ColorPicker.onRGBEditChanged, false) + end + + local hsvVlo = guiCreateVerticalLayout(0, 0, 0, 0, gs.mrg2, false, mainHlo) + for i, clr in ipairs({"h", "s", "v"}) do + + ColorPicker.gui.colors[clr] = {} + local hlo = guiCreateHorizontalLayout(0, 0, 0, 0, 0, false, hsvVlo) + + ColorPicker.gui.colors[clr].lbl = guiCreateLabel(0, 0, gs.lbl.w, gs.btn.h, utf8.upper(clr)..":", false, hlo) + guiLabelSetVerticalAlign(ColorPicker.gui.colors[clr].lbl, "center") + + ColorPicker.gui.colors[clr].edit = guiCreateEdit(0, 0, gs.edit.w, gs.btn.h, "0", false, hlo) + guiEditSetMatcher(ColorPicker.gui.colors[clr].edit, guiGetInputNumericMatcher(true, 0, clr == "h" and 360 or 100, 0)) + guiEditSetParser(ColorPicker.gui.colors[clr].edit, guiGetInputNumericParser(true, 0, clr == "h" and 360 or 100)) + guiSetInternalData(ColorPicker.gui.colors[clr].edit, "type", clr) + addEventHandler("onClientGUIChanged", ColorPicker.gui.colors[clr].edit, ColorPicker.onHSVEditChanged, false) + end + + local barVlo = guiCreateVerticalLayout(0, 0, 0, 0, gs.mrg2, false, mainHlo) + for i, clr in ipairs({"h", "s", "v"}) do + + ColorPicker.gui.colors[clr].barImg = guiCreateStaticImage(0, 0, gs.bar.w, gs.btn.h, GUI_IMAGES_PATH..(clr == "h" and "colorpicker/hue" or "util/white")..".png", false, barVlo) + guiSetProperty(ColorPicker.gui.colors[clr].barImg, "InheritsAlpha", "False") + + ColorPicker.gui.colors[clr].cursorImg = guiCreateStaticImage(0, 0, 0, 0, GUI_IMAGES_PATH.."colorpicker/hsvcursor.png", false, ColorPicker.gui.colors[clr].barImg) + guiStaticImageSetSizeNative(ColorPicker.gui.colors[clr].cursorImg) + guiSetVerticalAlign(ColorPicker.gui.colors[clr].cursorImg, "center") + guiSetHorizontalAlign(ColorPicker.gui.colors[clr].cursorImg, "center") + guiSetProperty(ColorPicker.gui.colors[clr].cursorImg, "ClippedByParent", "False") + guiSetEnabled(ColorPicker.gui.colors[clr].cursorImg, false) + end + + guiRebuildLayouts(ColorPicker.gui.window) + local w, h = guiGetSize(mainHlo, false) + guiSetSize(ColorPicker.gui.window, w + gs.mrg*2, h + gs.mrg*2, false) + + --------------------------------------------------------------------- + + guiBindKey(ColorPicker.gui.ok, "enter") + guiBindKey(ColorPicker.gui.cancel, "c") + + addEventHandler("onClientGUIMouseDown", ColorPicker.gui.window, ColorPicker.onMouseDown) + addEventHandler("onClientClick", root, ColorPicker.onMouseUp) + addEventHandler("onClientCursorMove", root, ColorPicker.onMouseMove) + + addEventHandler("onClientGUILeftClick", ColorPicker.gui.currentImg, ColorPicker.showHexEdit, false) + addEventHandler("onClientGUIBlur", ColorPicker.gui.colors.hex.edit, ColorPicker.hideHexEdit) + + addEventHandler("onClientGUIBlur", ColorPicker.gui.window, ColorPicker.onBlur, false) + addEventHandler("onClientGUILeftClick", ColorPicker.gui.cancel, ColorPicker.cancel, false) + addEventHandler("onClientGUILeftClick", ColorPicker.gui.ok, ColorPicker.accept, false) + + return true +end + +function ColorPicker.update() + + local color = ColorPicker.color + color.r, color.g, color.b = hsv2rgb(color.h, color.s, color.v) + color.hex = rgb2hexs(color.r, color.g, color.b) + + ColorPicker.editAntiRecursion = true + + guiSetText(ColorPicker.gui.colors.h.edit, tostring(math.round(color.h*360))) + guiSetText(ColorPicker.gui.colors.s.edit, tostring(math.round(color.s*100))) + guiSetText(ColorPicker.gui.colors.v.edit, tostring(math.round(color.v*100))) + guiSetText(ColorPicker.gui.colors.r.edit, tostring(color.r)) + guiSetText(ColorPicker.gui.colors.g.edit, tostring(color.g)) + guiSetText(ColorPicker.gui.colors.b.edit, tostring(color.b)) + guiSetText(ColorPicker.gui.colors.hex.edit, utf8.gsub(color.hex, "#", "")) + + guiSetXPosition(ColorPicker.gui.colors.h.cursorImg, color.h-0.5, true) + guiSetXPosition(ColorPicker.gui.colors.s.cursorImg, color.s-0.5, true) + guiSetXPosition(ColorPicker.gui.colors.v.cursorImg, color.v-0.5, true) + + local ceguihex = guiToCEGUIColor(hsv2rgb(color.h, 1, color.v)) + local ceguihex2 = guiToCEGUIColor(hsv2rgb(color.h, 0, color.v)) + guiSetProperty(ColorPicker.gui.colors.s.barImg, "ImageColours", "tl:"..ceguihex2.." tr:"..ceguihex.." bl:"..ceguihex2.." br:"..ceguihex) + + ceguihex = guiToCEGUIColor(hsv2rgb(color.h, color.s, 1)) + guiSetProperty(ColorPicker.gui.colors.v.barImg, "ImageColours", "tl:FF000000 tr:"..ceguihex.." bl:FF000000 br:"..ceguihex) + + guiSetText(ColorPicker.gui.colors.hex.lbl, color.hex) + guiStaticImageSetColor(ColorPicker.gui.currentImg, color.r, color.g, color.b) + + if color.v < 0.5 then + guiLabelSetColor(ColorPicker.gui.colors.hex.lbl, 255, 255, 255) + else + guiLabelSetColor(ColorPicker.gui.colors.hex.lbl, 0, 0, 0) + end + + ColorPicker.color = color + ColorPicker.editAntiRecursion = false + + return true +end + +function ColorPicker.prepare(element, r, g, b) + + local x, y = guiGetAbsolutePosition(element) + local sx, sy = guiGetSize(element, false) + + x = x + sx + 2 + y = y - 10 + + guiSetPosition(ColorPicker.gui.window, x, y, false) + + ColorPicker.color.r, ColorPicker.color.g, ColorPicker.color.b = r, g, b + ColorPicker.color.h, ColorPicker.color.s, ColorPicker.color.v = rgb2hsv(r, g ,b) + + ColorPicker.editAntiRecursion = false + ColorPicker.update() + + return true +end + +function ColorPicker.resume() + if not ColorPicker.thread then return false end + + guiSetEnabled(ColorPicker.gui.window, false) + guiSetVisible(ColorPicker.gui.window, false) + + coroutine.resume(ColorPicker.thread) +end + +function ColorPicker.cancel() + ColorPicker.accepted = false + ColorPicker.resume() +end + +function ColorPicker.accept() + ColorPicker.accepted = true + ColorPicker.resume() +end + +function ColorPicker.showHexEdit() + guiSetVisible(ColorPicker.gui.currentImg, false) + guiSetVisible(ColorPicker.gui.colors.hex.edit, true) + guiFocus(ColorPicker.gui.colors.hex.edit) +end + +function ColorPicker.hideHexEdit() + guiSetVisible(ColorPicker.gui.colors.hex.edit, false) + guiSetVisible(ColorPicker.gui.currentImg, true) +end + +function ColorPicker.onMouseDown(button) + if button ~= "left" then return end + + for i, clr in ipairs({"h", "s", "v"}) do + if ColorPicker.gui.colors[clr].barImg == source then + ColorPicker.picking = clr + ColorPicker.onMouseMove() + break + end + end +end + +function ColorPicker.onMouseUp(button, state) + if button ~= "left" then return end + if state ~= "up" then return end + + ColorPicker.picking = false +end + +function ColorPicker.onBlur() + if not guiGetEnabled(ColorPicker.gui.window) then return false end + + ColorPicker.cancel() +end + +function ColorPicker.onMouseMove() + if not ColorPicker.picking then return end + + local cx = getCursorAbsolutePosition() + local x = guiGetAbsolutePosition(ColorPicker.gui.colors[ColorPicker.picking].barImg) + local w = guiGetSize(ColorPicker.gui.colors[ColorPicker.picking].barImg, false) + ColorPicker.color[ColorPicker.picking] = (math.clamp(cx, x, x + w) - x)/w + ColorPicker.update() +end + +function ColorPicker.onRGBEditChanged() + if ColorPicker.editAntiRecursion then return end + + local value = guiEditGetParsedValue(source) + if not value then return end + + local color = guiGetInternalData(source, "type") + ColorPicker.color[color] = value + + ColorPicker.color.h, ColorPicker.color.s, ColorPicker.color.v = + rgb2hsv(ColorPicker.color.r, ColorPicker.color.g, ColorPicker.color.b) + + ColorPicker.update() + +end + +function ColorPicker.onHSVEditChanged() + if ColorPicker.editAntiRecursion then return end + + local value = guiEditGetParsedValue(source) + if not value then return end + + local color = guiGetInternalData(source, "type") + value = value/(color == "h" and 360 or 100) + + ColorPicker.color[color] = value + + ColorPicker.update() + +end + +function ColorPicker.onHexEditChanged() + if ColorPicker.editAntiRecursion then return end + + local text = guiGetText(source) + if #text > 6 then + text = utf8.sub(text, 1, 6) + end + while #text < 6 do + text = text.."0" + end + guiSetText(source, text) + + ColorPicker.color.h, ColorPicker.color.s, ColorPicker.color.v = rgb2hsv(hexs2rgb("#"..text)) + ColorPicker.update() +end + +ColorPicker.init() + +function ColorPicker.onElementClick() + + local element = this + + if ColorPicker.thread then ColorPicker.cancel() end + + ColorPicker.thread = coroutine.running() + ColorPicker.accepted = false + + local r, g, b = unpack(ColorPicker.elements[element].color) + ColorPicker.prepare(element, r, g, b) + guiSetVisible(ColorPicker.gui.window, true, true) + guiSetEnabled(ColorPicker.gui.window, true) + coroutine.yield() + + ColorPicker.thread = nil + + if not ColorPicker.accepted then return end + + r, g, b = ColorPicker.color.r, ColorPicker.color.g, ColorPicker.color.b + guiColorPickerSetColor(element, r, g, b) + triggerEvent("onClientGUIColorPickerAccepted", element, r, g, b) +end + +function ColorPicker.onElementDestroy() + ColorPicker.elements[source] = nil +end + +function guiCreateColorPicker(x, y, width, height, r, g, b, relative, parent) + if not scheck("n[4],?byte[3],b,?u:element:gui") then return false end + + local edit = guiCreateEdit(x, y, width, height, "", relative, parent) + if not edit then return false end + + guiEditSetReadOnly(edit, true) + + ColorPicker.elements[edit] = {} + guiColorPickerSetColor(edit, r, g, b) + + addEventHandler("onClientGUILeftClick", edit, ColorPicker.onElementClick, false) + addEventHandler("onClientElementDestroy", edit, ColorPicker.onElementDestroy, false) + + return edit +end + +function guiColorPickerSetColor(picker, r, g, b) + if not scheck("u:element:gui-edit,?byte[3]") then return false end + if not ColorPicker.elements[picker] then return false end + + local empty = not r and true or false + + r = r or 255 + g = g or 255 + b = b or 255 + + guiEditSetReadOnlyBackGroundColor(picker, r, g, b) + guiSetText(picker, empty and "N/A" or rgb2hexs(r, g, b)) + local h, s, v = rgb2hsv(r, g, b) + guiSetColorProperty(picker, "NormalTextColour", unpack((v < 0.5) and {255,255,255} or {0,0,0})) + + ColorPicker.elements[picker].color = {r, g, b} + ColorPicker.elements[picker].empty = empty + + return true +end + +function guiColorPickerGetColor(picker) + if not scheck("u:element:gui-edit") then return false end + if not ColorPicker.elements[picker] then return false end + + if ColorPicker.elements[picker].empty then return nil end + + return unpack(ColorPicker.elements[picker].color) +end \ No newline at end of file diff --git a/gui/lib/widgets/context.lua b/gui/lib/widgets/context.lua new file mode 100644 index 0000000..bf512d3 --- /dev/null +++ b/gui/lib/widgets/context.lua @@ -0,0 +1,145 @@ + +local gs = {} +gs.minw = 120 +gs.h = 20 +gs.mrg = 5 +gs.clr = {} +gs.alpha = 0.8 +gs.clr.normal = {0, 0, 0} +gs.clr.hover = {118, 143, 246} + +local Context = {} + +Context.contexts = {} + +function Context.onDestroy() + + for i, element in ipairs(guiGetInternalData(this, "attached")) do + Context.dettach(element) + end + +end + +function Context.onClick() + + guiSetVisible(this, false) + +end + +function Context.create() + + local context = guiCreateStaticImage(0, 0, 0, 0, GUI_WHITE_IMAGE_PATH, false) + guiSetVisible(context, false) + guiSetAlpha(context, gs.alpha) + guiStaticImageSetColor(context, table.unpack(gs.clr.normal)) + + local layout = guiCreateVerticalLayout(gs.mrg, 0, 0, 0, 0, false, context) + + guiSetInternalData(context, "layout", layout) + guiSetInternalData(context, "items", {}) + guiSetInternalData(context, "attached", {}) + table.insert(Context.contexts, context) + + addEventHandler("onClientGUILeftClick", context, Context.onClick) + addEventHandler("onClientElementDestroy", context, Context.onDestroy) + + return context +end + +function Context.onElementRightClick(cursorX, cursorY) + if getElementType(this) == "gui-gridlist" and guiGridListGetSelectedItem(this) == -1 then return end + + local context = guiGetInternalData(this, "context") + guiSetPosition(context, cursorX + 1, cursorY, false) + guiSetVisible(context, true, true) + +end + +function Context.onElementHide() + + guiSetVisible(guiGetInternalData(this, "context"), false) + +end + +function Context.onElementDestroy() + + Context.dettach(this) + +end + +function Context.attach(context, element) + if guiGetInternalData(element, "context") == context then return false end + + local attached = guiGetInternalData(context, "attached") + table.insert(attached, element) + guiSetInternalData(context, "attached", attached) + guiSetInternalData(element, "context", context) + + addEventHandler("onClientGUIRightClick", element, Context.onElementRightClick, false, "low") + addEventHandler("onClientGUIHide", element, Context.onElementHide) + addEventHandler("onClientElementDestroy", element, Context.onElementDestroy) + + return true +end + +function Context.dettach(element) + + local context = guiGetInternalData(element, "context") + if not context then return false end + + local attached = guiGetInternalData(context, "attached") + table.removevalue(attached, element) + guiSetInternalData(context, "attached", attached) + + guiSetInternalData(element, "context", nil) + removeEventHandler("onClientGUIRightClick", element, Context.onElementRightClick) + removeEventHandler("onClientGUIHide", element, Context.onElementHide) + removeEventHandler("onClientElementDestroy", element, Context.onElementDestroy) + + return true +end + +function Context.addItem(context, text) + + local layout = guiGetInternalData(context, "layout") + local item = guiCreateLabel(0, 0, 0, gs.h, text, false, layout) + guiLabelAdjustWidth(item) + guiLabelSetHoverColor(item, table.unpack(gs.clr.hover)) + guiLabelSetVerticalAlign(item, "center") + + guiVerticalLayoutRebuild(layout) + local w, h = guiGetSize(layout, false) + guiSetSize(context, math.max(gs.minw, w + gs.mrg*2), h, false) + + return item +end + +------------------------------------ + +function guiCreateContextMenu() + + return Context.create() +end + +function guiGetContextMenu(element) + if not scheck("u:element:gui") then return false end + + return guiGetInternalData(element, "context") +end + +function guiSetContextMenu(element, context) + if not scheck("u:element:gui,?u:element:gui") then return false end + if context and not table.index(Context.contexts, context) then return false end + + Context.dettach(element) + + if not context then return true end + return Context.attach(context, element) +end + +function guiContextMenuAddItem(context, text) + if not scheck("u:element:gui,s") then return false end + if not table.index(Context.contexts, context) then return false end + + return Context.addItem(context, text) +end \ No newline at end of file diff --git a/gui/lib/widgets/imagedialog.lua b/gui/lib/widgets/imagedialog.lua new file mode 100644 index 0000000..7831f38 --- /dev/null +++ b/gui/lib/widgets/imagedialog.lua @@ -0,0 +1,144 @@ + +local gs = {} +gs.w = 640 +gs.h = 480 +gs.columns = 3 +gs.mrgT = 20 +gs.mrg = 5 +gs.mrg2 = gs.mrg * 2 +gs.btn = {} +gs.btn.w = 80 +gs.btn.h = 20 + +local ImageDialog = {} + +ImageDialog.gui = {} +ImageDialog.thread = nil +ImageDialog.focused = nil +ImageDialog.result = nil +ImageDialog.accepted = nil + +function ImageDialog.init() + + ImageDialog.gui.window = guiCreateWindow(0, 0, 1, 1, "", true) + guiSetVisible(ImageDialog.gui.window, false) + guiWindowSetSizable(ImageDialog.gui.window, false) + + ImageDialog.gui.vlo = guiCreateVerticalLayout(gs.mrg2, gs.mrgT + gs.mrg2, GUI_SCREEN_WIDTH, GUI_SCREEN_HEIGHT, gs.mrg2, false, ImageDialog.gui.window) + guiVerticalLayoutSetHorizontalAlign(ImageDialog.gui.vlo, "center") + + ImageDialog.gui.list = guiCreateImageGridList(0, 0, gs.w, gs.h, false, ImageDialog.gui.vlo) + guiImageGridListSetColumns(ImageDialog.gui.list, gs.columns) + + local hlo = guiCreateHorizontalLayout(0, 0, 0, 0, gs.mrg, false, ImageDialog.gui.vlo) + ImageDialog.gui.ok = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Ok", false, hlo) + ImageDialog.gui.cancel = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Cancel", false, hlo) + + guiRebuildLayouts(ImageDialog.gui.window) + local w, h = guiGetSize(ImageDialog.gui.vlo, false) + guiSetSize(ImageDialog.gui.window, w + gs.mrg2*2, h + gs.mrg2*2 + gs.mrgT) + + guiSetEnabled(ImageDialog.gui.window, false) + + guiBindKey(ImageDialog.gui.ok, "enter") + guiBindKey(ImageDialog.gui.cancel, "c") + guiBindKey(ImageDialog.gui.cancel, "n") + + addEventHandler("onClientGUILeftClick", ImageDialog.gui.ok, ImageDialog.accept, false) + addEventHandler("onClientGUILeftClick", ImageDialog.gui.cancel, ImageDialog.cancel, false) + + return true +end + +function ImageDialog.prepare(title, data, limit, viewEnabled, columns) + + guiSetText(ImageDialog.gui.window, title or "") + + guiSetEnabled(ImageDialog.gui.window, true) + + local list = ImageDialog.gui.list + guiImageGridListClear(list) + guiImageGridListSetColumns(list, columns or gs.columns) + guiImageGridListSetSelectionLimit(list, limit or -1) + guiImageGridListSetViewEnabled(list, viewEnabled == nil and true or viewEnabled) + + for i, itemData in ipairs(data) do + local item = guiImageGridListAddItem(list, itemData.path) + if item then + if itemData.selected then guiImageGridListSetItemSelected(list, item, true) end + if itemData.data then guiImageGridListSetItemData(list, item, itemData.data) end + end + end + + guiCenter(ImageDialog.gui.window) + guiSetEnabled(ImageDialog.gui.window, false) + + return true +end + +function ImageDialog.resume() + if not ImageDialog.thread then return end + + guiSetEnabled(ImageDialog.gui.window, false) + guiSetVisible(ImageDialog.gui.window, false) + + coroutine.resume(ImageDialog.thread) +end + +function ImageDialog.getResult() + + local result = {} + for i, item in ipairs(guiImageGridListGetSelectedItems(ImageDialog.gui.list)) do + result[i] = { + item = item, + data = guiImageGridListGetItemData(ImageDialog.gui.list, item) + } + end + + return result +end + +function ImageDialog.accept() + + ImageDialog.result = ImageDialog.getResult() + ImageDialog.accepted = true + ImageDialog.resume() + + return true +end + +function ImageDialog.cancel() + + ImageDialog.accepted = false + ImageDialog.resume() + + return true +end + +ImageDialog.init() + +function guiShowImageDialog(title, data, limit, viewEnabled, columns) + if not scheck("?s,t,?n,?b,?n") then return false end + + if ImageDialog.thread then ImageDialog.cancel() end + + coroutine.sleep(10) + + ImageDialog.thread = coroutine.running() + ImageDialog.accepted = false + ImageDialog.focused = guiGetFocused() + + ImageDialog.prepare(title, data, limit, viewEnabled, columns) + guiSetEnabled(ImageDialog.gui.window, true) + guiSetVisible(ImageDialog.gui.window, true, true) + coroutine.yield() + + local accepted = ImageDialog.accepted + local result = ImageDialog.result + ImageDialog.thread = nil + coroutine.sleep(10) + + guiFocus(ImageDialog.focused) + + return accepted and result or false +end diff --git a/gui/lib/widgets/imagelist.lua b/gui/lib/widgets/imagelist.lua new file mode 100644 index 0000000..c44bf8d --- /dev/null +++ b/gui/lib/widgets/imagelist.lua @@ -0,0 +1,374 @@ + +addEvent("onClientGUIImageGridListItemSelected", false) + +local DEFAULT_COLUMNS = 2 +local DEFAULT_SELECTION_MODE = 2 +local DEFAULT_VIEW_ENABLED = false +local FORCE_VERTICAL_SCROLLBAR = true +local LOAD_IMAGE_INTERVAL = 100 + +local gs = {} +gs.spacing = 5 +gs.clr = {} +gs.clr.selected = {118, 143, 246, 153} +gs.clr.background = {127, 127, 127, 127} +gs.scrollsize = 20 +gs.btn = {} +gs.btn.x = 5 +gs.btn.y = 5 +gs.btn.w = 40 +gs.btn.h = 20 + +local ImageList = {} + +ImageList.data = {} + +function ImageList.create(x, y, width, height, relative, parent) + + local pane = guiCreateScrollPane(x, y, width, height, relative, parent) + if not pane then return false end + + guiSetBooleanProperty(pane, "ForceVertScrollbar", FORCE_VERTICAL_SCROLLBAR) + local layout = guiCreateGridLayout(0, 0, 0, 0, DEFAULT_COLUMNS, gs.spacing, false, pane) + + local data = {} + data.columns = DEFAULT_COLUMNS + data.selection = DEFAULT_SELECTION_MODE + data.limit = -1 + data.layout = layout + data.containers = {} + data.view = DEFAULT_VIEW_ENABLED + ImageList.data[pane] = data + ImageList.rebuild(pane) + + addEventHandler("onClientElementDestroy", pane, ImageList.onDestroy, false) + addEventHandler("onClientGUIScrollPaneVerticalScroll", pane, ImageList.onScroll, false) + + return pane +end + +function ImageList.onDestroy() + ImageList.data[this] = nil +end + +function ImageList.isContainerVisible(list, grid, container) + + local _, lh = guiGetSize(list, false) + local cy = guiGetYPosition(container, false) - (guiScrollPaneGetVerticalScrollPosition(list)/100 * (guiGetHeight(grid, false) - lh)) + 100 + local ch = guiGetHeight(container, false) + + return cy < lh and cy + ch > 0 +end + +function ImageList.load(list) + + local data = ImageList.data[list] + data.loading = true + + while data.load and data.loading do + if not isElement(list) then return true end + + data.load = false + + local containers = {} + for item, container in ipairs(data.containers) do + if not guiGetInternalData(container, "loaded") and + ImageList.isContainerVisible(list, data.layout, container) then + containers[#containers + 1] = container + end + end + + for i, container in ipairs(containers) do + if isElement(container) then + guiSetInternalData(container, "loaded", true) + local image = guiGetInternalData(container, "image") + guiStaticImageLoadImage(image, guiGetInternalData(container, "path")) + guiStaticImageScale(image, data.size, data.size) + coroutine.sleep(LOAD_IMAGE_INTERVAL) + end + end + end + + data.loading = false + + return true +end + +function ImageList.requestLoad(list) + + local data = ImageList.data[list] + if data.load then return end + + data.load = true + if not data.loading then coroutine.wrap(ImageList.load)(list) end + + return true +end + +function ImageList.setSelected(list, container, state) + + local data = ImageList.data[list] + local mask = guiGetInternalData(container, "mask") + + if state then + if data.selection == 1 then + for i, c in ipairs(data.containers) do + guiStaticImageSetColor(guiGetInternalData(c, "mask"), 0, 0, 0, 0) + guiSetInternalData(c, "selected", false) + end + else + if data.limit > -1 then + local selectedCount = 0 + for i, c in ipairs(data.containers) do + if guiGetInternalData(c, "selected") then selectedCount = selectedCount + 1 end + end + if selectedCount >= data.limit then return end + end + end + guiStaticImageSetColor(mask, table.unpack(gs.clr.selected)) + else + guiStaticImageSetColor(mask, 0, 0, 0, 0) + end + + return guiSetInternalData(container, "selected", state) +end + +function ImageList.onScroll() + + ImageList.requestLoad(this) + +end + +function ImageList.onClickItem() + + local list = guiGetInternalData(this, "list") + if ImageList.data[list].selection == 0 then return end + + ImageList.setSelected(list, this, not guiGetInternalData(this, "selected")) + +end + +function ImageList.onClickView() + + local container = getElementParent(this) + local path = guiGetInternalData(container, "path") + guiShowImageViewer(path) + +end + +function ImageList.rebuild(list) + + local data = ImageList.data[list] + + data.size = math.floor((guiGetSize(list, false) - gs.spacing * (data.columns - 1) - gs.scrollsize)/data.columns) + for item, container in ipairs(data.containers) do + guiSetSize(container, data.size, data.size, false) + guiStaticImageScale(guiGetInternalData(container, "image"), data.size, data.size) + end + guiGridLayoutSetColumns(data.layout, data.columns) + + return true +end + +function ImageList.clear(list) + + local data = ImageList.data[list] + for item, container in ipairs(data.containers) do + destroyElement(container) + end + data.load = false + data.loading = false + data.containers = {} + ImageList.rebuild(list) + + return true +end + +function ImageList.add(list, path) + + local data = ImageList.data[list] + local container = guiCreateContainer(0, 0, data.size, data.size, false, data.layout) + local image = guiCreateStaticImage(0, 0, 1, 1, GUI_EMPTY_IMAGE_PATH, true, container) + + guiStaticImageSetColor(container, table.unpack(gs.clr.background)) + guiSetInternalData(container, "list", list) + guiSetInternalData(container, "selected", false) + + guiSetHorizontalAlign(image, "center") + guiSetVerticalAlign(image, "center") + guiSetEnabled(image, false) + guiSetInternalData(container, "image", image) + guiSetInternalData(container, "path", path) + + local mask = guiCreateStaticImage(0, 0, 1, 1, GUI_WHITE_IMAGE_PATH, true, container) + guiSetEnabled(mask, false) + guiStaticImageSetColor(mask, 0, 0, 0, 0) + guiSetInternalData(container, "mask", mask) + + local view = guiCreateButton(gs.btn.x, gs.btn.y, gs.btn.w, gs.btn.h, "View", false, container) + guiSetEnabled(view, data.view) + guiSetVisible(view, data.view) + guiSetInternalData(container, "view", view) + + addEventHandler("onClientGUILeftClick", container, ImageList.onClickItem, false) + addEventHandler("onClientGUILeftClick", view, ImageList.onClickView, false) + + local item = #data.containers + 1 + data.containers[item] = container + + guiGridLayoutRebuild(data.layout) + ImageList.requestLoad(list) + + return item +end + +function ImageList.setViewEnabled(list, state) + + local data = ImageList.data[list] + data.view = state + + for item, container in ipairs(data.containers) do + local btn = guiGetInternalData(container, "view") + guiSetEnabled(btn, state) + guiSetVisible(btn, state) + end + + return true +end + +---------------------------------- + +function guiCreateImageGridList(x, y, width, height, relative, parent) + if not scheck("n[4],b,?u:element:gui") then return false end + + return ImageList.create(x, y, width, height, relative, parent) +end + +function guiImageGridListSetColumns(list, columns) + if not scheck("u:element:gui,?n") then return false end + if not ImageList.data[list] then return false end + + columns = math.floor(columns) + if columns < 1 then return false end + + ImageList.data[list].columns = columns + ImageList.rebuild(list) + + return true +end + +-- 0 - selection not allowed, 1 - single selection, 2 - multiple selection (default) +function guiImageGridListSetSelectionMode(list, selection) + if not scheck("u:element:gui,?n") then return false end + if not ImageList.data[list] then return false end + + selection = math.floor(selection) + if selection < 0 or selection > 2 then return false end + + ImageList.data[list].selection = selection + + return true +end + +function guiImageGridListGetSelectionMode(list) + if not scheck("u:element:gui") then return false end + if not ImageList.data[list] then return false end + + return ImageList.data[list].selection +end + +function guiImageGridListSetSelectionLimit(list, limit) + if not scheck("u:element:gui,?n") then return false end + if not ImageList.data[list] then return false end + + limit = math.floor(limit) + if limit < -1 then return false end + + ImageList.data[list].limit = limit + + return true +end + +function guiImageGridListGetSelectionLimit(list) + if not scheck("u:element:gui") then return false end + if not ImageList.data[list] then return false end + + return ImageList.data[list].limit +end + +function guiImageGridListSetViewEnabled(list, state) + if not scheck("u:element:gui,b") then return false end + if not ImageList.data[list] then return false end + if ImageList.data[list].view == state then return false end + + return ImageList.setViewEnabled(list, state) +end + +function guiImageGridListClear(list) + if not scheck("u:element:gui") then return false end + if not ImageList.data[list] then return false end + + return ImageList.clear(list) +end + +function guiImageGridListAddItem(list, path) + if not scheck("u:element:gui,s") then return false end + if not ImageList.data[list] then return false end + if not fileExists(path) then return false end + + return ImageList.add(list, path) +end + +function guiImageGridListSetItemSelected(list, item, state) + if not scheck("u:element:gui,n,b") then return false end + if not ImageList.data[list] then return false end + + local container = ImageList.data[list].containers[item] + if not container then return false end + if guiGetInternalData(container, "selected") == state then return false end + + return ImageList.setSelected(list, container, state) +end + +function guiImageGridListGetSelectedItem(list) + if not scheck("u:element:gui") then return false end + if not ImageList.data[list] then return false end + + for item, container in ipairs(ImageList.data[list].containers) do + if guiGetInternalData(container, "selected") then return item end + end + return 0 +end + +function guiImageGridListGetSelectedItems(list) + if not scheck("u:element:gui") then return false end + if not ImageList.data[list] then return false end + + local selected = {} + for item, container in ipairs(ImageList.data[list].containers) do + if guiGetInternalData(container, "selected") then table.insert(selected, item) end + end + return selected +end + +function guiImageGridListGetItemData(list, item) + if not scheck("u:element:gui,n") then return false end + if not ImageList.data[list] then return false end + + local container = ImageList.data[list].containers[item] + if not container then return false end + + return guiGetInternalData(container, "data") +end + +function guiImageGridListSetItemData(list, item, data) + if not scheck("u:element:gui,n") then return false end + if not ImageList.data[list] then return false end + if not ImageList.data[list].containers[item] then return false end + + local container = ImageList.data[list].containers[item] + if not container then return false end + + return guiSetInternalData(container, "data", data) +end + diff --git a/gui/lib/widgets/imageviewer.lua b/gui/lib/widgets/imageviewer.lua new file mode 100644 index 0000000..c6b3be5 --- /dev/null +++ b/gui/lib/widgets/imageviewer.lua @@ -0,0 +1,142 @@ + +local ZOOM_SPEED = 0.1 + +local gs = {} +gs.mrgT = 20 +gs.mrg = 10 +gs.btn = {} +gs.btn.w = 80 +gs.btn.h = 20 +gs.maxw = GUI_SCREEN_WIDTH - 80 - gs.mrg*2 +gs.maxh = GUI_SCREEN_HEIGHT - 80 - (gs.mrg*3 + gs.mrgT + gs.btn.h) + +local ImageViewer = {} + +ImageViewer.gui = {} +ImageViewer.big = false +ImageViewer.moving = false + +function ImageViewer.init() + + ImageViewer.gui.window = guiCreateWindow(0, 0, gs.maxw, gs.maxh, "Image viewer", false) + guiSetVisible(ImageViewer.gui.window, false) + ImageViewer.gui.layout = guiCreateVerticalLayout(gs.mrg, gs.mrg + gs.mrgT, 0, 0, gs.mrg, false, ImageViewer.gui.window) + guiVerticalLayoutSetHorizontalAlign(ImageViewer.gui.layout, "center") + + ImageViewer.gui.container = guiCreateContainer(0, 0, 0, 0, false, ImageViewer.gui.layout) + ImageViewer.gui.img = guiCreateStaticImage(0, 0, 1, 1, GUI_EMPTY_IMAGE_PATH, true, ImageViewer.gui.container) + ImageViewer.gui.okBtn = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Ok", false, ImageViewer.gui.layout) + + guiBindKey(ImageViewer.gui.okBtn, "enter") + + addEventHandler("onClientGUILeftClick", ImageViewer.gui.okBtn, ImageViewer.hide, false) + addEventHandler("onClientMouseWheel", ImageViewer.gui.img, ImageViewer.onScroll, false) + addEventHandler("onClientGUIMouseDown", ImageViewer.gui.img, ImageViewer.onMouseDown, false) + addEventHandler("onClientClick", root, ImageViewer.onMouseUp) + addEventHandler("onClientCursorMove", root, ImageViewer.onMouseMove) + + return true +end + +function ImageViewer.show(path) + + if not guiStaticImageLoadImage(ImageViewer.gui.img, path) then return false end + + local w, h = guiStaticImageGetNativeSize(ImageViewer.gui.img) + guiSetMaxSize(ImageViewer.gui.img, w, h, false) + + ImageViewer.big = false + if w > gs.maxw then + h = h * (gs.maxw/w) + w = gs.maxw + ImageViewer.big = true + end + if h > gs.maxh then + w = w * (gs.maxh/h) + h = gs.maxh + ImageViewer.big = true + end + ImageViewer.maxw = w + ImageViewer.maxh = h + + guiSetSize(ImageViewer.gui.container, w, h, false) + guiSetPosition(ImageViewer.gui.img, 0, 0, false) + guiSetSize(ImageViewer.gui.img, w, h, false) + guiVerticalLayoutRebuild(ImageViewer.gui.layout) + local lw, lh = guiGetSize(ImageViewer.gui.layout, false) + guiSetSize(ImageViewer.gui.window, lw + gs.mrg*2, lh + gs.mrg*2 + gs.mrgT, false) + guiCenter(ImageViewer.gui.window) + guiSetVisible(ImageViewer.gui.window, true, true) + showCursor(true) + +end + +function ImageViewer.hide() + + return guiSetVisible(ImageViewer.gui.window, false) +end + +function ImageViewer.onScroll(vector) + if not ImageViewer.big then return false end + + local nw, nh = guiStaticImageGetNativeSize(ImageViewer.gui.img) + local w, h = guiGetSize(ImageViewer.gui.img, false) + local w2 = math.clamp(w * (1 + ZOOM_SPEED * vector), ImageViewer.maxw, nw) + local h2 = math.clamp(h * (1 + ZOOM_SPEED * vector), ImageViewer.maxh, nh) + + local wscale = w2/w + local hscale = h2/h + if wscale == 1 and hscale == 1 then return end + + guiSetSize(ImageViewer.gui.img, w2, h2, false) + + local cx, cy = getCursorAbsolutePosition() + local ax, ay = guiGetAbsolutePosition(ImageViewer.gui.img) + local x, y = guiGetPosition(ImageViewer.gui.img, false) + local x2 = math.clamp(x - ((cx - ax) * (wscale - 1)), ImageViewer.maxw - w2, 0) + local y2 = math.clamp(y - ((cy - ay) * (hscale - 1)), ImageViewer.maxh - h2, 0) + guiSetPosition(ImageViewer.gui.img, x2, y2, false) + +end + +function ImageViewer.onMouseDown(button, x, y) + if button ~= "left" then return end + if not ImageViewer.big then return end + + ImageViewer.cursorX = x + ImageViewer.cursorY = y + ImageViewer.moving = true + +end + +function ImageViewer.onMouseUp(button, state) + if state ~= "up" then return end + if button ~= "left" then return end + + ImageViewer.moving = false + +end + +function ImageViewer.onMouseMove(_, _, cx, cy) + if not ImageViewer.big then return end + if not ImageViewer.moving then return end + + local x, y = guiGetPosition(ImageViewer.gui.img, false) + local w, h = guiGetSize(ImageViewer.gui.img, false) + local x2 = math.clamp(x + (cx - ImageViewer.cursorX), ImageViewer.maxw - w, 0) + local y2 = math.clamp(y + (cy - ImageViewer.cursorY), ImageViewer.maxh - h, 0) + guiSetPosition(ImageViewer.gui.img, x2, y2, false) + + ImageViewer.cursorX = cx + ImageViewer.cursorY = cy + +end + +ImageViewer.init() + +function guiShowImageViewer(path) + if not scheck("s") then return false end + if not fileExists(path) then return false end + + return ImageViewer.show(path) +end \ No newline at end of file diff --git a/gui/lib/widgets/inputdialog.lua b/gui/lib/widgets/inputdialog.lua new file mode 100644 index 0000000..55f54c6 --- /dev/null +++ b/gui/lib/widgets/inputdialog.lua @@ -0,0 +1,139 @@ + +local gs = {} +gs.w = 300 +gs.mrg = 10 +gs.mrg2 = 5 +gs.btn = {} +gs.btn.w = 80 +gs.btn.h = 20 +gs.edit = {} +gs.edit.h = 25 + +local InputDialog = {} + +InputDialog.gui = {} +InputDialog.thread = nil +InputDialog.focused = nil +InputDialog.result = nil +InputDialog.accepted = nil +InputDialog.allowInvalid = false + +function InputDialog.init() + InputDialog.gui.window = guiCreateWindow(0, 0, gs.w, 20 + gs.edit.h * 2 + gs.btn.h + gs.mrg * 4, "", false) + guiSetEnabled(InputDialog.gui.window, false) + guiSetVisible(InputDialog.gui.window, false) + guiWindowSetSizable(InputDialog.gui.window, false) + guiSetAlwaysOnTop(InputDialog.gui.window, true) + + InputDialog.gui.lbl = guiCreateLabel(0, 0, 0, 0, "", false, InputDialog.gui.window) + guiLabelSetHorizontalAlign(InputDialog.gui.lbl, "left", true) + + InputDialog.gui.edit = guiCreateEdit(0, 0, gs.w - gs.mrg*2, gs.edit.h, "", false, InputDialog.gui.window) + + InputDialog.gui.ok = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Ok", false, InputDialog.gui.window) + InputDialog.gui.cancel = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Cancel", false, InputDialog.gui.window) + + guiBindKey(InputDialog.gui.ok, "enter") + guiBindKey(InputDialog.gui.cancel, "c") + guiBindKey(InputDialog.gui.cancel, "n") + + addEventHandler("onClientGUIAccepted", InputDialog.gui.edit, InputDialog.accept, false) + addEventHandler("onClientGUILeftClick", InputDialog.gui.ok, InputDialog.accept, false) + addEventHandler("onClientGUILeftClick", InputDialog.gui.cancel, InputDialog.cancel, false) +end + +function InputDialog.prepare(title, message, value, parser, matcher) + + guiSetText(InputDialog.gui.window, title or "") + guiSetText(InputDialog.gui.lbl, (message or "Enter the value")..":") + guiSetText(InputDialog.gui.edit, value ~= nil and tostring(value) or "") + guiEditSetCaretIndex(InputDialog.gui.edit) + guiEditSetParser(InputDialog.gui.edit, parser) + guiEditSetMatcher(InputDialog.gui.edit, matcher) + + local lblw, lblh = guiGetTextSize(InputDialog.gui.lbl, gs.w - gs.mrg * 2 - gs.mrg2) + lblw = gs.w - gs.mrg * 2 - gs.mrg2 + + local winh = 20 + lblh + gs.edit.h + gs.btn.h + gs.mrg * 3 + gs.mrg2 + guiSetSize(InputDialog.gui.window, gs.w, winh, false) + guiCenter(InputDialog.gui.window) + + local x = gs.mrg + local y = 20 + gs.mrg + guiSetPosition(InputDialog.gui.lbl, x + gs.mrg2, y, false) + guiSetSize(InputDialog.gui.lbl, lblw, lblh, false) + + y = y + lblh + gs.mrg2 + guiSetPosition(InputDialog.gui.edit, x, y, false) + + x = (gs.w - gs.btn.w*2 - gs.mrg)/2 + y = y + gs.edit.h + gs.mrg + guiSetPosition(InputDialog.gui.ok, x, y, false) + + x = x + gs.btn.w + gs.mrg + guiSetPosition(InputDialog.gui.cancel, x, y, false) + + return true +end + +function InputDialog.resume() + if not InputDialog.thread then return end + + guiSetEnabled(InputDialog.gui.window, false) + guiSetVisible(InputDialog.gui.window, false) + + coroutine.resume(InputDialog.thread) +end + +function InputDialog.accept() + + InputDialog.result = table.pack(guiEditGetParsedValue(InputDialog.gui.edit)) + if (not InputDialog.allowInvalid) and (not InputDialog.result[1]) then + return guiShowMessageDialog("Invalid value", nil, MD_OK, MD_ERROR) and false + end + + InputDialog.accepted = true + InputDialog.resume() + + return true +end + +function InputDialog.cancel() + + InputDialog.accepted = false + InputDialog.resume() + + return true +end + +InputDialog.init() + +function guiShowInputDialog(title, message, value, parser, allowInvalid, matcher) + if not scheck("?s[2],?s|n|b,?f,?b,?f") then return false end + + if InputDialog.thread then InputDialog.cancel() end + + coroutine.sleep(10) + + InputDialog.thread = coroutine.running() + InputDialog.accepted = false + InputDialog.focused = guiGetFocused() + InputDialog.allowInvalid = allowInvalid + + InputDialog.prepare(title, message, value, parser, matcher) + guiSetVisible(InputDialog.gui.window, true, true) + guiSetEnabled(InputDialog.gui.window, true) + guiFocus(InputDialog.gui.edit) + coroutine.yield() + + local accepted = InputDialog.accepted + local data = InputDialog.result + InputDialog.thread = nil + coroutine.sleep(10) + + guiFocus(InputDialog.focused) + + if not accepted then return false end + + return table.unpack(data) +end diff --git a/gui/lib/widgets/messagedialog.lua b/gui/lib/widgets/messagedialog.lua new file mode 100644 index 0000000..63b8b88 --- /dev/null +++ b/gui/lib/widgets/messagedialog.lua @@ -0,0 +1,184 @@ + +enum({ + "MD_OK", + "MD_OK_CANCEL", + "MD_YES_NO", + "MD_YES_NO_CANCEL" +}) + +enum({ + "MD_ERROR", + "MD_WARNING", + "MD_QUESTION", + "MD_INFO", + "MD_PLAIN" +}) + +local gs = {} +gs.ratio = 3/2 +gs.w = 400 +gs.mrg = 10 +gs.btn = {} +gs.btn.w = 80 +gs.btn.h = 20 +gs.img = {} +gs.img.w = 42 +gs.img.h = 42 +gs.headers = { + [MD_ERROR] = "Error", + [MD_WARNING] = "Warning", + [MD_QUESTION] = "Question", + [MD_INFO] = "Information" +} +gs.btn.text = { + [MD_OK] = {"ok"}, + [MD_OK_CANCEL] = {"ok", "cancel"}, + [MD_YES_NO] = {"yes", "no"}, + [MD_YES_NO_CANCEL] = {"yes", "no", "cancel"} +} + +local MessageDialog = {} + +MessageDialog.gui = {} +MessageDialog.thread = nil +MessageDialog.focused = nil +MessageDialog.options = {} +MessageDialog.result = nil + +function MessageDialog.init() + + MessageDialog.gui.window = guiCreateWindow(0, 0, gs.w, 20 + gs.img.h + gs.mrg * 5, "", false) + guiSetEnabled(MessageDialog.gui.window, false) + guiSetVisible(MessageDialog.gui.window, false) + guiWindowSetSizable(MessageDialog.gui.window, false) + guiSetAlwaysOnTop(MessageDialog.gui.window, true) + + MessageDialog.gui.img = guiCreateStaticImage(0, 0, gs.img.w, gs.img.h, GUI_EMPTY_IMAGE_PATH, false, MessageDialog.gui.window) + MessageDialog.gui.infoImg = guiCreateStaticImage(0, 0, 1, 1, GUI_IMAGES_PATH.."dialog/info.png", true, MessageDialog.gui.img) + MessageDialog.gui.questionImg = guiCreateStaticImage(0, 0, 1, 1, GUI_IMAGES_PATH.."dialog/question.png", true, MessageDialog.gui.img) + MessageDialog.gui.warningImg = guiCreateStaticImage(0, 0, 1, 1, GUI_IMAGES_PATH.."dialog/warning.png", true, MessageDialog.gui.img) + MessageDialog.gui.errorImg = guiCreateStaticImage(0, 0, 1, 1, GUI_IMAGES_PATH.."dialog/error.png", true, MessageDialog.gui.img) + + MessageDialog.gui.lbl = guiCreateLabel(0, 0, gs.w, gs.img.h, "", false, MessageDialog.gui.window) + guiLabelSetHorizontalAlign(MessageDialog.gui.lbl, "center", true) + guiLabelSetVerticalAlign(MessageDialog.gui.lbl, "center") + + MessageDialog.gui.btns = {} + MessageDialog.gui.btns.yes = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Yes", false, MessageDialog.gui.window) + MessageDialog.gui.btns.no = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "No", false, MessageDialog.gui.window) + MessageDialog.gui.btns.ok = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Ok", false, MessageDialog.gui.window) + MessageDialog.gui.btns.cancel = guiCreateButton(0, 0, gs.btn.w, gs.btn.h, "Cancel", false, MessageDialog.gui.window) + + guiBindKey(MessageDialog.gui.btns.yes, "enter") + guiBindKey(MessageDialog.gui.btns.no, "n") + guiBindKey(MessageDialog.gui.btns.ok, "enter") + guiBindKey(MessageDialog.gui.btns.cancel, "c") + + addEventHandler("onClientGUILeftClick", MessageDialog.gui.btns.yes, MessageDialog.yes, false) + addEventHandler("onClientGUILeftClick", MessageDialog.gui.btns.no, MessageDialog.no, false) + addEventHandler("onClientGUILeftClick", MessageDialog.gui.btns.ok, MessageDialog.yes, false) + addEventHandler("onClientGUILeftClick", MessageDialog.gui.btns.cancel, MessageDialog.cancel, false) + + return true +end + +function MessageDialog.prepare(message, header, optionType, iconType) + if not optionType then optionType = MD_OK end + if not iconType then iconType = MD_INFO end + + guiSetText(MessageDialog.gui.window, header or gs.headers[iconType] or "") + guiSetText(MessageDialog.gui.lbl, message) + + guiSetVisible(MessageDialog.gui.warningImg, iconType == MD_WARNING) + guiSetVisible(MessageDialog.gui.questionImg, iconType == MD_QUESTION) + guiSetVisible(MessageDialog.gui.errorImg, iconType == MD_ERROR) + guiSetVisible(MessageDialog.gui.infoImg, iconType == MD_INFO) + + local btns = gs.btn.text[optionType] + local btnsw = (gs.btn.w + gs.mrg) * #btns - gs.mrg + local lblw, lblh = guiGetTextSize(MessageDialog.gui.lbl, gs.w) + lblw = math.max(lblw, btnsw, gs.w) + lblh = math.max(lblh, gs.img.h + gs.mrg*2) + + local winw = gs.img.w + lblw + gs.mrg*6 + local winh = 20 + lblh + gs.btn.h + gs.mrg*3 + guiSetSize(MessageDialog.gui.window, winw, winh, false) + guiCenter(MessageDialog.gui.window) + + local x = gs.mrg*2 + local y = 20 + gs.mrg + (lblh - gs.img.h) / 2 + guiSetPosition(MessageDialog.gui.img, x, y, false) + + x = x + gs.img.w + gs.mrg*2 + y = 20 + gs.mrg + guiSetPosition(MessageDialog.gui.lbl, x, y, false) + guiSetSize(MessageDialog.gui.lbl, lblw, lblh, false) + + for name, btn in pairs(MessageDialog.gui.btns) do + guiSetEnabled(btn, false) + guiSetVisible(btn, false) + end + + x = gs.img.w + gs.mrg*4 + (lblw - btnsw)/2 + y = winh - gs.mrg - gs.btn.h + for i, name in ipairs(btns) do + guiSetVisible(MessageDialog.gui.btns[name], true) + guiSetEnabled(MessageDialog.gui.btns[name], true) + guiSetPosition(MessageDialog.gui.btns[name], x, y, false) + x = x + gs.btn.w + gs.mrg + end + + return true +end + +function MessageDialog.resume() + if not MessageDialog.thread then return end + + guiSetEnabled(MessageDialog.gui.window, false) + guiSetVisible(MessageDialog.gui.window, false) + + coroutine.resume(MessageDialog.thread) +end + +function MessageDialog.yes() + MessageDialog.result = true + MessageDialog.resume() +end + +function MessageDialog.no() + MessageDialog.result = false + MessageDialog.resume() +end + +function MessageDialog.cancel() + MessageDialog.result = nil + MessageDialog.resume() +end + +MessageDialog.init() + +function guiShowMessageDialog(message, header, optionType, iconType) + if not scheck("s,?s") then return false end + + if MessageDialog.thread then MessageDialog.cancel() end + + coroutine.sleep(10) + + MessageDialog.thread = coroutine.running() + MessageDialog.result = false + MessageDialog.focused = guiGetFocused() + + MessageDialog.prepare(message, header, optionType, iconType) + guiSetVisible(MessageDialog.gui.window, true, true) + guiSetEnabled(MessageDialog.gui.window, true) + guiFocus(MessageDialog.gui.window) + + coroutine.yield() + + MessageDialog.thread = nil + + coroutine.sleep(10) + guiFocus(MessageDialog.focused) + + return MessageDialog.result +end \ No newline at end of file diff --git a/gui/lib/widgets/tooltip.lua b/gui/lib/widgets/tooltip.lua new file mode 100644 index 0000000..089f258 --- /dev/null +++ b/gui/lib/widgets/tooltip.lua @@ -0,0 +1,123 @@ + +local SHOW_DELAY = 0.5 * 10^3 + +local gs = {} +gs.w = 240 +gs.pdn = 5 +gs.clr = {} +gs.clr.bg = COLORS.black +gs.clr.text = COLORS.yellow + +local ToolTip = {} + +ToolTip.gui = {} +ToolTip.element = nil +ToolTip.timer = nil + +function ToolTip.onRender() + if not ToolTip.element then return end + if isCursorShowing() and guiGetVisible(ToolTip.element) then return end + + ToolTip.element = nil + guiSetVisible(ToolTip.gui.container, false) + +end + +function ToolTip.init() + + ToolTip.gui.container = guiCreateStaticImage(0, 0, 200, 200, GUI_WHITE_IMAGE_PATH, false) + guiStaticImageSetColor(ToolTip.gui.container, unpack(gs.clr.bg)) + guiSetAlpha(ToolTip.gui.container, 0.8) + guiSetAlwaysOnTop(ToolTip.gui.container, true) + guiSetVisible(ToolTip.gui.container, false) + guiSetEnabled(ToolTip.gui.container, false) + + ToolTip.gui.label = guiCreateLabel(0, 0, 0, 0, "", false, ToolTip.gui.container) + guiLabelSetHorizontalAlign(ToolTip.gui.label, "left", true) + + addEventHandler("onClientGUIRender", ToolTip.gui.container, ToolTip.onRender, false) + + return true +end + +ToolTip.init() + +function ToolTip.show() + if not ToolTip.element then return end + if not isCursorShowing() then return end + if guiGetVisible(ToolTip.gui.container) then return end + + local text = guiGetCustomProperty(ToolTip.element, "tooltip") + if not text then return end + + local color = guiGetCustomProperty(ToolTip.element, "tooltipTextColour") + if not color then color = gs.clr.text end + + guiSetText(ToolTip.gui.label, text) + guiLabelSetColor(ToolTip.gui.label, unpack(color)) + + local w, h = guiGetTextSize(ToolTip.gui.label, gs.w) + guiSetSize(ToolTip.gui.container, w + gs.pdn*2, h + (gs.pdn-2)*2, false) + guiSetPosition(ToolTip.gui.label, gs.pdn, (gs.pdn-2), false) + guiSetSize(ToolTip.gui.label, w, h, false) + + local cx, cy = getCursorAbsolutePosition() + guiSetPosition(ToolTip.gui.container, cx + 1, cy + 14, false) + guiSetVisible(ToolTip.gui.container, true, true) + + return true +end + +addEventHandler("onClientMouseEnter", guiRoot, + function() + if not guiGetCustomProperty(source, "tooltip") then return end + + ToolTip.element = source + end +) + +addEventHandler("onClientMouseLeave", guiRoot, + function() + if ToolTip.element ~= source then return end + + ToolTip.element = nil + end +) + +addEventHandler("onClientMouseMove", guiRoot, + function() + if guiGetVisible(ToolTip.gui.container) then guiSetVisible(ToolTip.gui.container, false) end + + if ToolTip.element ~= source then return end + if ToolTip.timer and isTimer(ToolTip.timer) then return resetTimer(ToolTip.timer) end + + ToolTip.timer = setTimer(ToolTip.show, SHOW_DELAY, 1) + end +) + +addEventHandler("onClientElementDestroy", guiRoot, + function() + if source ~= ToolTip.element then return end + + ToolTip.element = nil + guiSetVisible(ToolTip.gui.container, false) + end +) + +addEventHandler("onClientGUIClick", guiRoot, + function() + guiSetVisible(ToolTip.gui.container, false) + end +) + +function guiSetToolTip(element, text, r, g, b) + if not scheck("u:element:gui,?s,?byte[3]") then return false end + + if r and g and b then + guiSetCustomProperty(element, "tooltipTextColour", {r, g, b}) + else + guiSetCustomProperty(element, "tooltipTextColour") + end + + return guiSetCustomProperty(element, "tooltip", text) +end \ No newline at end of file diff --git a/gui/style.lua b/gui/style.lua new file mode 100644 index 0000000..afa3fa0 --- /dev/null +++ b/gui/style.lua @@ -0,0 +1,355 @@ + +-- GLOBAL STYLE +GS = {} + +GS.nilText = "N/A" + +GS.clr = {} +GS.clr.red = {255, 127, 127} +GS.clr.orange = {255, 127, 63} +GS.clr.yellow = {255, 255, 127} +GS.clr.green = {127, 255, 127} +GS.clr.blue = {127, 127, 255} +GS.clr.aqua = {127, 255, 255} +GS.clr.white = {255, 255, 255} +GS.clr.grey = {127, 127, 127} +GS.clr.black = {0, 0, 0} + +GS.mrgT = 20 -- top margin of window +GS.mrg = 5 +GS.mrg2 = GS.mrg * 2 +GS.mrg3 = GS.mrg2 * 2 + +GS.w = 40 +GS.w2 = GS.w * 2 + GS.mrg +GS.w21 = GS.w2 + GS.mrg + GS.w +GS.w3 = GS.w2 * 2 + GS.mrg +GS.w31 = GS.w3 + GS.mrg + GS.w +GS.w32 = GS.w3 + GS.mrg + GS.w2 +GS.w4 = GS.w3 * 2 + GS.mrg + +GS.h = 20 +GS.h2 = GS.h * 2 + GS.mrg +GS.h21 = GS.h2 + GS.h + GS.mrg +GS.h3 = GS.h2 * 2 + GS.mrg +GS.h31 = GS.h3 + GS.h + GS.mrg +GS.h32 = GS.h3 + GS.h2 + GS.mrg +GS.h4 = GS.h3 * 2 + GS.mrg + +GS.scroll = {} +GS.scroll.size = 20 + +GS.button = {} +GS.button.w = GS.w2 +GS.button.h = GS.h +GS.button.font = "default-bold-small" +GS.button.normalTextColor = {guiButtonGetNormalTextColor(GUI_DEFAULT_BUTTON)} +GS.button.hoverTextColor = {guiButtonGetHoverTextColor(GUI_DEFAULT_BUTTON)} +GS.button.disabledTextColor = {guiButtonGetDisabledTextColor(GUI_DEFAULT_BUTTON)} + +GS.checkbox = {} +GS.checkbox.w = GS.w2 +GS.checkbox.h = GS.h + +GS.edit = {} +GS.edit.w = GS.w2 +GS.edit.h = GS.h + +GS.gridlist = {} +GS.gridlist.w = GS.w3 +GS.gridlist.item = {} +GS.gridlist.item.color = {guiGridListGetItemColor(GUI_DEFAULT_GRIDLIST, 0, 1)} + +GS.memo = {} +GS.memo.w = GS.w32 +GS.memo.h = GS.h2 + +GS.scrollpane = {} +GS.scrollpane.w = GS.w3 +GS.scrollpane.h = GS.h2 + +GS.label = {} +GS.label.w = GS.w2 +GS.label.h = GS.h +GS.label.verticalAlign = "center" +GS.label.disabledColor = GS.clr.grey + +GS.window = {} +GS.window.visible = false +GS.window.sizable = false +GS.window.center = true + +GS.separator = {} +GS.separator.color = {guiSeparatorGetColor(GUI_DEFAULT_HORIZONTALSEPARATOR)} + +GS.hlo = {} +GS.hlo.spacing = GS.mrg + +GS.vlo = {} +GS.vlo.spacing = GS.mrg + +GS.colorpicker = {} +GS.colorpicker.w = GS.w2 +GS.colorpicker.h = GS.h +GS.colorpicker.color = GS.clr.white + + +local _guiSetText = guiSetText +function guiSetText(element, text) + if not scheck("u:element:gui") then return false end + + return _guiSetText( + element, + text and tostring(text) or (getElementType(element) == "gui-edit" and "" or GS.nilText) + ) +end + +local function prepareArguments(style, x, y, width, height, text, relative, parent) + + x = x or ((not relative) and style.x or 0) + y = y or ((not relative) and style.y or 0) + width = width or ((not relative) and style.w or 0) + height = height or ((not relative) and style.h or 0) + text = text and tostring(text) or GS.nilText + + return x, y, width, height, text, relative or false, parent +end + +local function applyStyle(element, style) + + if style.font then guiSetFont(element, style.font) end + if style.visible == false then guiSetVisible(element, false) end + if style.center then guiCenter(element) end + + return true +end + +function guiButtonSetStyle(button, style) + if not scheck("u:element:gui-button,?t") then return false end + + style = style or GS.button + + applyStyle(button, style) + if style.normalTextColor then guiButtonSetNormalTextColor(button, table.unpack(style.normalTextColor)) end + if style.hoverTextColor then guiButtonSetHoverTextColor(button, table.unpack(style.hoverTextColor)) end + if style.disabledTextColor then guiButtonSetDisabledTextColor(button, table.unpack(style.disabledTextColor)) end + + return true +end + +local _guiCreateButton = guiCreateButton +function guiCreateButton(x, y, width, height, text, relative, parent) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local style = GS.button or {} + local element = _guiCreateButton(prepareArguments(style, x, y, width, height, text, relative, parent)) + if not element then return element end + + guiButtonSetStyle(element, style) + + return element +end + +local _guiCreateCheckBox = guiCreateCheckBox +function guiCreateCheckBox(x, y, width, height, text, selected, relative, parent) + if not scheck("?n[4],?s,?b[2],?u:element:gui") then return false end + + local style = GS.checkbox or {} + x, y, width, height, text, relative = prepareArguments(style, x, y, width, height, text, relative) + local element = _guiCreateCheckBox(x, y, width, height, text, selected, relative, parent) + if not element then return element end + + applyStyle(element, style) + + return element +end + +local _guiCreateEdit = guiCreateEdit +function guiCreateEdit(x, y, width, height, text, relative, parent) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local style = GS.edit or {} + local element = _guiCreateEdit(prepareArguments(style, x, y, width, height, text, relative, parent)) + if not element then return element end + + applyStyle(element, style) + + return element +end + +local _guiCreateMemo = guiCreateMemo +function guiCreateMemo(x, y, width, height, text, relative, parent) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local style = GS.memo or {} + local element = _guiCreateMemo(prepareArguments(style, x, y, width, height, text, relative, parent)) + if not element then return element end + + applyStyle(element, style) + + return element +end + +local _guiCreateScrollPane = guiCreateScrollPane +function guiCreateScrollPane(x, y, width, height, relative, parent) + if not scheck("?n[4],?b,?u:element:gui") then return false end + + local style = GS.scrollpane or {} + x, y, width, height, _, relative = prepareArguments(style, x, y, width, height, nil, relative) + local element = _guiCreateScrollPane(x, y, width, height, relative, parent) + if not element then return element end + + applyStyle(element, style) + + return element +end + +local _guiCreateGridList = guiCreateGridList +function guiCreateGridList(x, y, width, height, relative, parent) + if not scheck("?n[4],?b,?u:element:gui") then return false end + + local style = GS.gridlist or {} + x, y, width, height, _, relative = prepareArguments(style, x, y, width, height, nil, relative) + local element = _guiCreateGridList(x, y, width, height, relative, parent) + if not element then return element end + + applyStyle(element, style) + + return element +end + +local _guiGridListSetItemText = guiGridListSetItemText +function guiGridListSetItemText(list, row, column, text, isSection, isNumber) + if not scheck("u:element:gui-gridlist,n,n,?s|n|b,?b[2]") then return false end + + local set = _guiGridListSetItemText(list, row, column, text == nil and GS.nilText or tostring(text), isSection, isNumber) + if not set then return set end + + guiGridListSetItemColor(list, row, column, table.unpack(text == nil and GS.clr.grey or GS.gridlist.item.color)) + + return set +end + +local _guiCreateLabel = guiCreateLabel +function guiCreateLabel(x, y, width, height, text, relative, parent) + if not scheck("?n[4],?s,?b,?u:element:gui") then return false end + + local style = GS.label or {} + local element = _guiCreateLabel(prepareArguments(style, x, y, width, height, text, relative, parent)) + if not element then return element end + + applyStyle(element, style) + guiLabelSetVerticalAlign(element, style.verticalAlign) + guiLabelSetDisabledColor(element, table.unpack(style.disabledColor)) + + return element +end + + +local _guiCreateWindow = guiCreateWindow +function guiCreateWindow(x, y, width, height, text, relative) + if not scheck("?n[4],?s,?b") then return false end + + local style = GS.window or {} + local element = _guiCreateWindow(prepareArguments(style, x, y, width, height, text, relative)) + if not element then return element end + + applyStyle(element, style) + guiWindowSetSizable(element, style.sizable) + + return element +end + +local _guiCreateTabPanel = guiCreateTabPanel +function guiCreateTabPanel(x, y, width, height, relative, parent) + if not scheck("?n[4],?b,?u:element:gui") then return false end + + local style = GS.tabpanel or {} + x, y, width, height, _, relative = prepareArguments(style, x, y, width, height, nil, relative) + local element = _guiCreateTabPanel(x, y, width, height, relative, parent) + if not element then return element end + + applyStyle(element, style) + + return element +end + +local _guiCreateHorizontalSeparator = guiCreateHorizontalSeparator +function guiCreateHorizontalSeparator(x, y, length, relative, parent) + if not scheck("?n[3],?b,?u:element:gui") then return false end + + local style = GS.separator or {} + x, y, length, _, _, relative = prepareArguments(style, x, y, length, nil, nil, relative) + local element = _guiCreateHorizontalSeparator(x, y, length, relative, parent) + if not element then return element end + + applyStyle(element, style) + guiSeparatorSetColor(element, table.unpack(style.color)) + + return element +end + +local _guiCreateVerticalSeparator = guiCreateVerticalSeparator +function guiCreateVerticalSeparator(x, y, length, relative, parent) + if not scheck("?n[3],?b,?u:element:gui") then return false end + + local style = GS.separator or {} + x, y, _, length, _, relative = prepareArguments(style, x, y, nil, length, nil, relative) + local element = _guiCreateVerticalSeparator(x, y, length, relative, parent) + if not element then return element end + + applyStyle(element, style) + guiSeparatorSetColor(element, table.unpack(style.color)) + + return element +end + +local _guiCreateHorizontalLayout = guiCreateHorizontalLayout +function guiCreateHorizontalLayout(x, y, width, height, spacing, relative, parent) + if not scheck("?n[5],?b,?u:element:gui") then return false end + + local style = GS.hlo or {} + x, y, width, height, _, relative = prepareArguments(style, x, y, width, height, nil, relative) + spacing = spacing or style.spacing or 0 + + local element = _guiCreateHorizontalLayout(x, y, width, height, spacing, relative, parent) + if not element then return element end + + applyStyle(element, style) + + return element +end + +local _guiCreateVerticalLayout = guiCreateVerticalLayout +function guiCreateVerticalLayout(x, y, width, height, spacing, relative, parent) + if not scheck("?n[5],?b,?u:element:gui") then return false end + + local style = GS.vlo or {} + x, y, width, height, _, relative = prepareArguments(style, x, y, width, height, nil, relative) + spacing = spacing or style.spacing or 0 + + local element = _guiCreateVerticalLayout(x, y, width, height, spacing, relative, parent) + if not element then return element end + + applyStyle(element, style) + + return element +end + +local _guiCreateColorPicker = guiCreateColorPicker +function guiCreateColorPicker(x, y, width, height, r, g, b, relative, parent) + if not scheck("?n[4],?byte[3],?b,?u:element:gui") then return false end + + local style = GS.colorpicker or {} + x, y, width, height, _, relative = prepareArguments(style, x, y, width, height, nil, relative) + r = r or style.color[1] + g = g or style.color[2] + b = b or style.color[3] + + local element = _guiCreateColorPicker(x, y, width, height, r, g, b, relative, parent) + if not element then return element end + + applyStyle(element, style) + + return element +end \ No newline at end of file diff --git a/helpers/client/dx.lua b/helpers/client/dx.lua new file mode 100644 index 0000000..bfb3dca --- /dev/null +++ b/helpers/client/dx.lua @@ -0,0 +1,16 @@ + +function dxDrawBorderedText(text, border, left, top, right, bottom, color, scale, font, alignX, alignY, clip, wordBreak,postGUI) + + border = math.max(math.floor(border), 1) + right = right or left + bottom = bottom or top + font = font or "default" + + for bx = -border, border do + for by = -border, border do + dxDrawText(text, left + bx, top + by, right + bx, bottom + by, tocolor(0, 0, 0, 255), scale, font, alignX, alignY, clip, wordBreak,postGUI) + end + end + + return dxDrawText(text, left, top, right, bottom, color, scale, font, alignX, alignY, clip, wordBreak, postGUI) +end \ No newline at end of file diff --git a/helpers/color.lua b/helpers/color.lua new file mode 100644 index 0000000..54f8924 --- /dev/null +++ b/helpers/color.lua @@ -0,0 +1,63 @@ + +function hsv2rgb(h, s, v) + check("n[3]") + + local r, g, b + local i = math.floor(h * 6) + local f = h * 6 - i + local p = v * (1 - s) + local q = v * (1 - f * s) + local t = v * (1 - (1 - f) * s) + local switch = i % 6 + if switch == 0 then + r = v g = t b = p + elseif switch == 1 then + r = q g = v b = p + elseif switch == 2 then + r = p g = v b = t + elseif switch == 3 then + r = p g = q b = v + elseif switch == 4 then + r = t g = p b = v + elseif switch == 5 then + r = v g = p b = q + end + + return math.round(r*255), math.round(g*255), math.round(b*255) +end + +function rgb2hsv(r, g, b) + check("byte[3]") + + r, g, b = r/255, g/255, b/255 + local max, min = math.max(r, g, b), math.min(r, g, b) + local h, s + local v = max + local d = max - min + s = max == 0 and 0 or d/max + if max == min then + h = 0 + elseif max == r then + h = (g - b) / d + (g < b and 6 or 0) + elseif max == g then + h = (b - r) / d + 2 + elseif max == b then + h = (r - g) / d + 4 + end + h = h/6 + + return h, s, v +end + +function rgb2hexs(r, g, b, a) + check("byte[3],?byte") + + if a then return string.format("#%.2X%.2X%.2X%.2X", r, g, b, a) end + return string.format("#%.2X%.2X%.2X", r, g, b) +end + +function hexs2rgb(hexs) + check("s") + + return getColorFromString(hexs) +end \ No newline at end of file diff --git a/helpers/fightingstyles.lua b/helpers/fightingstyles.lua new file mode 100644 index 0000000..d901a5c --- /dev/null +++ b/helpers/fightingstyles.lua @@ -0,0 +1,43 @@ + +local styles = { + [4] = "Default", + [5] = "Boxing", + [6] = "Kung Fu", + [7] = "Knee-head", + [15] = "Grab kick", + [16] = "Elbows" +} + +function getValidFightingStyles() + + local list = {} + for style in pairs(styles) do + list[#list + 1] = style + end + table.sort(list) + + return list +end + +function isValidFightingStyle(style) + if not scheck("n") then return false end + + return styles[style] and true or false +end + +function getFightingStyleName(style) + if not scheck("n") then return false end + if not styles[style] then return warn("invalid fighting style", 2) and false end + + return styles[style] +end + +function getFightingStyleFromPartialName(partialName) + if not scheck("s") then return false end + + partialName = utf8.lower(partialName) + for style, name in pairs(styles) do + if utf8.find(utf8.lower(name), partialName, 1, true) then return style end + end + return nil +end \ No newline at end of file diff --git a/helpers/file.lua b/helpers/file.lua new file mode 100644 index 0000000..da81fcf --- /dev/null +++ b/helpers/file.lua @@ -0,0 +1,83 @@ + +function fileReadAll(file) + if not scheck("u:element:file") then return false end + + fileSetPos(file, 0) + local data = fileRead(file, fileGetSize(file)) + fileSetPos(file, 0) + + return data +end + +function fileReadLine(file) + if not scheck("u:element:file") then return false end + + if fileIsEOF(file) then return false end + + local line = "" + local char = "" + while not (utf8.byte(char) == 10 or fileIsEOF(file)) do + line = line..char + char = fileRead(file, 1) + end + + return line +end + +function fileClampByLine(path, lines) + if not scheck("s,n") then return false end + + if lines < 1 then return false end + + local file = fileOpen(path, true) + if not file then return false end + + local size = fileGetSize(file) + local pos = {0} + while not fileIsEOF(file) do + if utf8.byte(fileRead(file, 1)) == 10 then table.insert(pos, fileGetPos(file)) end + end + fileClose(file) + + if #pos <= lines then return true end + + return fileClamp(path, size - pos[#pos - lines]) +end + +function fileClamp(path, bytes) + if not scheck("s,n") then return false end + + local file = fileOpen(path, true) + if not file then return false end + + local size = fileGetSize(file) + if size <= bytes then + fileClose(file) + return true + end + + fileSetPos(file, size - bytes) + local data = fileRead(file, bytes) + fileClose(file) + + if not fileDelete(path) then return false end + file = fileCreate(path) + if not file then return false end + + fileWrite(file, data) + fileFlush(file) + fileClose(file) + + return true +end + +function fileClear(path) + if not scheck("s") then return false end + + if not fileDelete(path) then return false end + local file = fileCreate(path) + if not file then return nil end + fileClose(file) + + return true +end \ No newline at end of file diff --git a/helpers/gameplay.lua b/helpers/gameplay.lua new file mode 100644 index 0000000..ad5f12a --- /dev/null +++ b/helpers/gameplay.lua @@ -0,0 +1,105 @@ + +function formatGameTime(hours, minutes) + if not scheck("n[2]") then return false end + + return string.format("%02d:%02d", hours, minutes) +end + +function formatColor(r, g, b) + if not scheck("?n[3]") then return false end + + return string.format("(%d, %d, %d)", r or 0, g or 0, b or 0) +end + +function formatPosition(x, y, z) + if not scheck("n[2],?n") then return false end + + return string.format(z and "(%.1f; %.1f; %.1f)" or "(%.1f; %.1f)", x, y, z) +end + +function parseGameTime(str) + if not scheck("s") then return false end + if not utf8.match(str, "^%d*:?%d*$") then return false end + + local h, m = utf8.match(str, "^(%d*):?(%d*)$") + if h == "" then h = nil end + if m == "" then m = nil end + + h = h and tonumber(h) + m = m and tonumber(m) + + return h, m +end + +function formatNameID(name, id) + if not scheck("s,?n|s") then return false end + + if not id then return name end + + return string.format("(%s) %s", tostring(id), name) +end + +function isValidAccountPassword(s) + if not scheck("s") then return false end + + local len = utf8.len(s) + return len >= 1 and len <= 30 and s ~= "*****" +end + +local _getPlayerName = getPlayerName +function getPlayerName(player, strip, bleach) + if not scheck("u:element:player|u:element:console|u:element:root,?b,?b") then return false end + + local name = (player == root or player == console) and "Console" or _getPlayerName(player) + + return (bleach and "#FFFFFF" or "")..(strip and stripColorCodes(name) or name) +end + +function getPlayerFromPartialName(partialName) + if not scheck("s") then return false end + + if partialName == "" then return false end + + local player = getPlayerFromName(partialName) + if player then return player end + + partialName = stripColorCodes(partialName) + local ignoreCasePartialName = utf8.lower(partialName) + + for i, p in ipairs(getElementsByType("player")) do + local playerName = stripColorCodes(getPlayerName(p)) + if playerName == partialName then return p end + if utf8.find(playerName, partialName, 1, true) then return p end + + local ignoreCasePlayerName = utf8.lower(playerName) + if ignoreCasePlayerName == ignoreCasePartialName then return p end + if utf8.find(ignoreCasePlayerName, ignoreCasePartialName, 1, true) then return p end + end + return nil +end + +function getPlayersByPartialName(partialName) + if not scheck("s") then return false end + + if partialName == "" then return false end + + local players = {} + partialName = utf8.lower(stripColorCodes(partialName)) + for i, p in ipairs(getElementsByType("player")) do + local playerName = utf8.lower(stripColorCodes(getPlayerName(p))) + if utf8.find(playerName, partialName, 1, true) then + players[#players + 1] = p + end + end + return players +end + +function getPositionFromElementOffset(element, offX, offY, offZ) + if not scheck("u:element,n[3]") then return false end + + local m = getElementMatrix(element, false) + local x = offX * m[1][1] + offY * m[2][1] + offZ * m[3][1] + m[4][1] + local y = offX * m[1][2] + offY * m[2][2] + offZ * m[3][2] + m[4][2] + local z = offX * m[1][3] + offY * m[2][3] + offZ * m[3][3] + m[4][3] + return x, y, z +end \ No newline at end of file diff --git a/helpers/glitches.lua b/helpers/glitches.lua new file mode 100644 index 0000000..ed05cdc --- /dev/null +++ b/helpers/glitches.lua @@ -0,0 +1,50 @@ + +local glitchesData = { + {"quickreload", "Quick reload"}, + {"fastmove", "Fast move"}, + {"fastfire", "Fast fire"}, + {"crouchbug", "Crouch bug"}, + {"highcloserangedamage", "High close range damage"}, + {"hitanim", "Hit anim"}, + {"fastsprint", "Fast sprint"}, + {"baddrivebyhitbox", "Bad driveby hit box"}, + {"quickstand", "Quick stand"}, + {"kickoutofvehicle_onmodelreplace", "Kick out of vehicle on model replace"}, +} + +function getValidGlitches() + local list = {} + for i, data in ipairs(glitchesData) do + table.insert(list, data[1]) + end + return list +end + +function isValidGlitch(glitch) + if not scheck("s") then return false end + + for i, data in ipairs(glitchesData) do + if data[1] == glitch then return true end + end + return false +end + +function getGlitchName(glitch) + if not scheck("s") then return false end + + for i, data in ipairs(glitchesData) do + if data[1] == glitch then return data[2] end + end + + return warn("invalid glitch", 2) and false +end + +function getGlitchFromPartialName(partialName) + if not scheck("s") then return false end + + partialName = utf8.lower(partialName) + for i, data in ipairs(glitchesData) do + if utf8.find(utf8.lower(data[2]), partialName, 1, true) then return data[1] end + end + return nil +end \ No newline at end of file diff --git a/helpers/hash.lua b/helpers/hash.lua new file mode 100644 index 0000000..ee76c34 --- /dev/null +++ b/helpers/hash.lua @@ -0,0 +1,24 @@ + +function crc32(...) + + local data = "" + for i = 1, select("#", ...) do + data = data..tostring(select(i, ...)) + end + + local crc = 0xFFFFFFFF + local byte = 0 + local mask = 0 + + for i = 1, #data do + byte = utf8.byte(data, i) + crc = bitXor(crc, byte) + for j = 1, 8 do + mask = bitAnd(crc, 1) * (-1) + crc = bitXor(bitRShift(crc, 1), bitAnd(0xEDB88320, mask)) + end + end + crc = bitNot(crc) + + return string.format("%08X", crc) +end \ No newline at end of file diff --git a/helpers/intlocs.lua b/helpers/intlocs.lua new file mode 100644 index 0000000..aa11060 --- /dev/null +++ b/helpers/intlocs.lua @@ -0,0 +1,98 @@ + +local path = "conf/interiors.xml" +local locationsData = {} + +local function loadData() + local fileNode = xmlLoadFile(path, true) + if not fileNode then return warn("interior locations conf file not found", 2) and false end + + local fileData = xmlNodeGetData(fileNode) + xmlUnloadFile(fileNode) + + for ig, groupData in ipairs(fileData.children) do + for il, locationData in ipairs(groupData.children) do + local data = { + name = locationData.attributes.name, + group = groupData.attributes.group, + int = tonumber(locationData.attributes.interior), + pos = { + tonumber(locationData.attributes.posX), + tonumber(locationData.attributes.posY), + tonumber(locationData.attributes.posZ) + }, + rot = tonumber(locationData.attributes.rot or 0) + } + table.insert(locationsData, data) + end + end + return true +end +loadData() + + +function isValidInteriorLocation(id) + if not scheck("n") then return false end + + return locationsData[id] and true or false +end + +function getValidInteriorLocations() + local locations = {} + for id = 1, #locationsData do table.insert(locations, id) end + return locations +end + +function getValidInteriorLocationGroups() + local groups = {} + for id, data in ipairs(locationsData) do + if not table.index(groups, data.group) then table.insert(groups, data.group) end + end + return table.sort(groups) +end + +function getInteriorLocationName(id) + if not scheck("n") then return false end + if not locationsData[id] then return warn("invalid interior location", 2) and false end + + return locationsData[id].name +end + +function getInteriorLocationGroup(id) + if not scheck("n") then return false end + if not locationsData[id] then return warn("invalid interior location", 2) and false end + + return locationsData[id].group +end + +function getInteriorLocationInterior(id) + if not scheck("n") then return false end + if not locationsData[id] then return warn("invalid interior location", 2) and false end + + return locationsData[id].int +end + +function getInteriorLocationPosition(id) + if not scheck("n") then return false end + if not locationsData[id] then return warn("invalid interior location", 2) and false end + + local x, y, z = unpack(locationsData[id].pos) + return x, y, z, locationsData[id].rot +end + +function getInteriorLocationsByGroup(group) + local locations = {} + for id, data in ipairs(locationsData) do + if data.group == group then table.insert(locations, id) end + end + return locations +end + +function getInteriorLocationFromPartialName(partial) + if not scheck("s") then return false end + + partial = utf8.lower(partial) + for id, data in ipairs(locationsData) do + if utf8.find(utf8.lower(data.name), partial, 1, true) then return id end + end + return nil +end \ No newline at end of file diff --git a/helpers/remote.lua b/helpers/remote.lua new file mode 100644 index 0000000..57493df --- /dev/null +++ b/helpers/remote.lua @@ -0,0 +1,34 @@ + +function isValidSerial(serial) + if not scheck("s") then return false end + + if utf8.len(serial) ~= 32 then return false end + if not utf8.match(serial, "^%x+$") then return false end + return true +end + +function isValidIPv4(ip) + if not scheck("s") then return false end + + if not utf8.match(ip, "^%d+%.%d+%.%d+%.%d+$") then return false end + for chunk in utf8.gmatch(ip, "%d+") do + if tonumber(chunk) > 255 then return false end + end + return true +end + +function isValidIPv6(ip) + if not scheck("s") then return false end + + if not utf8.match(ip, "^%x+:%x+:%x+:%x+:%x+:%x+:%x+:%x+$") then return false end + for chunk in utf8.gmatch(ip, "%x+") do + if tonumber(chunk, 16) > 65535 then return false end + end + return true +end + +function isValidIP(ip) + if not scheck("s") then return false end + + return isValidIPv4(ip) +end \ No newline at end of file diff --git a/helpers/server/acl.lua b/helpers/server/acl.lua new file mode 100644 index 0000000..e6e4890 --- /dev/null +++ b/helpers/server/acl.lua @@ -0,0 +1,108 @@ + +local validRightTypes = { + "general", + "function", + "resource", + "command" +} + +function aclIsValidRightType(rightType) + check("s") + + return table.index(validRightTypes, rightType) and true or false +end + +function aclIsValidRight(right) + check("s") + + local rightType = utf8.match(right, "^(%a+)%.%a+") + if not rightType then return false end + + return table.index(validRightTypes, rightType) and true or false +end + +function aclIsAuto(acl) + check("u:acl") + + return utf8.match(aclGetName(acl), "^autoACL_") and true or false +end + +function aclGroupIsAuto(group) + check("u:acl-group") + + return utf8.match(aclGroupGetName(group), "^autoGroup_") and true or false +end + +function aclInGroup(acl, group) + check("u:acl,u:acl-group") + + for i, groupACL in ipairs(aclGroupListACL(group)) do + if groupACL == acl then return true end + end + return false +end + +function aclRightExists(acl, right) + check("u:acl,s") + + for i, r in ipairs(aclListRights(acl)) do + if r == right then return true end + end + return false +end + +function hasGroupPermissionTo(group, permission) + check("u:acl-group,s") + + for i, acl in ipairs(aclGroupListACL(group)) do + if aclGetRight(acl, permission) then return true end + end + return false +end + +function getObjectACLName(object) + check("u:resource-data|u:element:player|u:element:console") + + if object == console then return "user.Console" end + + if getUserdataType(object) == "resource-data" then + return "resource."..getResourceName(object) + else + return "user."..(getPlayerAccountName(object) or "*") + end +end + +function getObjectACLGroups(object) + check("s|u:resource-data|u:element:player|u:element:console") + + if type(object) ~= "string" then object = getObjectACLName(object) end + + local groups = {} + for i, group in ipairs(aclGroupList()) do + if isObjectInACLGroup(object, group) then + groups[#groups + 1] = group + end + end + return groups +end + +function getObjectPermissions(object, allowedType) + check("s|u:resource-data|u:element:player|u:element:console,?s") + if allowedType and not table.index(validRightTypes, allowedType) then + return warn("invalid allowed type", 2) and false + end + + if type(object) ~= "string" then object = getObjectACLName(object) end + + local permissions = {} + for gi, group in ipairs(getObjectACLGroups(object)) do + for ai, acl in ipairs(aclGroupListACL(group)) do + for ri, right in ipairs(aclListRights(acl, allowedType)) do + if aclGetRight(acl, right) then + if not table.index(permissions, right) then table.insert(permissions, right) end + end + end + end + end + return permissions +end \ No newline at end of file diff --git a/helpers/server/db.lua b/helpers/server/db.lua new file mode 100644 index 0000000..9d0137d --- /dev/null +++ b/helpers/server/db.lua @@ -0,0 +1,15 @@ + +function dbPrepareBatchString(connection, query, rowsParams) + check("u:element:db-connection,s,t") + + local rows = #rowsParams + if rows == 0 then return false end + if rows == 1 then return dbPrepareString(connection, query, table.unpack(rowsParams[1])) end + + local batchQuery = "" + for i, params in ipairs(rowsParams) do + batchQuery = batchQuery..dbPrepareString(connection, query, table.unpack(params))..";" + end + + return batchQuery +end \ No newline at end of file diff --git a/helpers/server/gameplay.lua b/helpers/server/gameplay.lua new file mode 100644 index 0000000..cd169bd --- /dev/null +++ b/helpers/server/gameplay.lua @@ -0,0 +1,83 @@ + +console = getElementByIndex("console", 0) + +function getPlayerAccountName(player) + if not scheck("u:element:player|u:element:root|u:element:console") then return false end + + if player == root or player == console then return "Console" end + + local account = getPlayerAccount(player) + if not account or isGuestAccount(account) then return nil end + + return getAccountName(account) +end + +function getPlayerBySerial(serial) + if not scheck("s") then return false end + if not isValidSerial(serial) then return warn("invalid serial", 2) and false end + + for i, player in ipairs(getElementsByType("player")) do + if getPlayerSerial(player) == serial then return player end + end + return nil +end + +function getPlayerByIP(ip) + if not scheck("s") then return false end + if not isValidIP(ip) then return warn("invalid IP", 2) and false end + + for i, player in ipairs(getElementsByType("player")) do + if getPlayerIP(player) == ip then return player end + end + return nil +end + +function getBanBySerial(serial) + if not scheck("s") then return false end + if not isValidSerial(serial) then return warn("invalid serial", 2) and false end + + for i, ban in ipairs(getBans()) do + if getBanSerial(ban) == serial then return ban end + end + return nil +end + +function getBanByIP(ip) + if not scheck("s") then return false end + if not isValidIP(ip) then return warn("invalid IP", 2) and false end + + for i, ban in ipairs(getBans()) do + if getBanIP(ban) == ip then return ban end + end + return nil +end + +function getBan(serialIP) + if not scheck("s") then return false end + if not (isValidSerial(serialIP) or isValidIP(serialIP)) then return warn("invalid serial or IP", 2) and false end + + for i, ban in ipairs(getBans()) do + if getBanSerial(ban) == serialIP or getBanIP(ban) == serialIP then return ban end + end + return nil +end + +function getBanDuration(ban) + if not scheck("u:ban") then return false end + + local time = getBanTime(ban) + local unban = getUnbanTime(ban) + if (not unban) or (not time) or (unban <= time) then return 0 end + + return unban - time +end + +function setBanDuration(ban, seconds) + if not scheck("u:ban,n") then return false end + if seconds < 0 then return warn("invalid duration", 2) and false end + + seconds = math.floor(seconds) + if seconds == 0 then return setUnbanTime(ban, 0) end + + return setUnbanTime(ban, (getBanTime(ban) or 0) + seconds) +end \ No newline at end of file diff --git a/helpers/server/remote.lua b/helpers/server/remote.lua new file mode 100644 index 0000000..cb58f74 --- /dev/null +++ b/helpers/server/remote.lua @@ -0,0 +1,31 @@ + +remote = {} + +local threads = {} +local results = {} + +local function callback(responseData, error, cid) + results[cid] = responseData + coroutine.resume(threads[cid]) +end + +function remote.fetch(url, options) + if not scheck("s,?t") then return false end + + local c = coroutine.running() + local cid = address(c) + + local fetch = fetchRemote(url, options or {}, callback, {cid}) + if not fetch then return false end + + threads[cid] = c + coroutine.yield() + + local result = results[cid] + threads[cid] = nil + results[cid] = nil + + if result == "ERROR" then return false end + + return result +end diff --git a/helpers/server/resource.lua b/helpers/server/resource.lua new file mode 100644 index 0000000..cff9620 --- /dev/null +++ b/helpers/server/resource.lua @@ -0,0 +1,92 @@ + +function getResourceSetting(resource, setting) + if not scheck("?u:resource-data,s") then return false end + + resource = resource or getThisResource() + + local resourceName = getResourceName(resource) + local types = {"boolean", "number", "string", "table"} + local value = get(resourceName.."."..setting) + + if not table.index(types, type(value)) then return nil end + + return value +end + +function getResourceSettingsData(resource) + if not scheck("?u:resource-data") then return false end + + if not resource then resource = getThisResource() end + + local resourceName = getResourceName(resource) + local rawsettings = get("*"..resourceName..".") + + if not rawsettings then return {} end + + local settings = {} + local types = {"boolean", "number", "string", "table"} + for rawname, value in pairs(rawsettings) do + if table.index(types, type(value)) then + rawname = utf8.gsub(rawname, "[%*%#%@](.*)", "%1") + local name = utf8.gsub(rawname, resourceName.."%.(.*)", "%1") + + if settings[name] == nil then settings[name] = {} end + if rawname == name then + settings[name].default = value + else + settings[name].current = value + end + end + end + + local result = {} + for name, value in pairs(settings) do + if value.default ~= nil then + local data = {} + data.default = value.default + data.current = value.default + if value.current ~= nil then + data.current = value.current + end + data.friendlyname = get(resourceName.."."..name..".friendlyname") + data.group = get(resourceName.."."..name..".group") + data.accept = get(resourceName.."."..name..".accept") + data.examples = get(resourceName.."."..name..".examples") + data.desc = get(resourceName.."."..name..".desc") + result[name] = data + end + end + + return result +end + +function getResourceNameFromSetting(setting) + if not scheck("s") then return false end + + return utf8.match(setting, "^[%*%#%@]?(.+)%..+") +end + +local defaultInfoAttributes = { + "name", + "type", + "author", + "version", + "description" +} + +local _getResourceInfo = getResourceInfo + +function getResourceInfo(resource, attribute) + if not scheck("?u:resource-data,?s") then return false end + + if not resource then resource = getThisResource() end + if not attribute then + local info = {} + for i, attribute in ipairs(defaultInfoAttributes) do + info[attribute] = _getResourceInfo(resource, attribute) or nil + end + return info + end + + return _getResourceInfo(resource, attribute) +end \ No newline at end of file diff --git a/helpers/skins.lua b/helpers/skins.lua new file mode 100644 index 0000000..0186079 --- /dev/null +++ b/helpers/skins.lua @@ -0,0 +1,59 @@ + +local path = "conf/skins.xml" +local skinsData = {} + +local function loadData() + local fileNode = xmlLoadFile(path, true) + if not fileNode then return warn("skins conf file not found", 2) and false end + + local fileData = xmlNodeGetData(fileNode) + xmlUnloadFile(fileNode) + + for i, skinData in ipairs(fileData.children) do + local model = tonumber(skinData.attributes.model) + skinsData[model] = { + name = skinData.attributes.name, + groups = split(skinData.attributes.groups, ","), + walking = tonumber(skinData.attributes.walking) + } + end + return true +end +loadData() + +function isValidPedModel(model) + if not scheck("n") then return false end + + return skinsData[model] and true or false +end + +function getPedModelName(model) + if not scheck("n") then return false end + if not skinsData[model] then return warn("invalid ped model", 2) and false end + + return skinsData[model].name +end + +function getPedModelFromPartialName(partialName) + if not scheck("s") then return false end + + partialName = utf8.lower(partialName) + for model, data in pairs(skinsData) do + if utf8.find(utf8.lower(data.name), partialName, 1, true) then return model end + end + return nil +end + +function getPedModelGroups(model) + if not scheck("n") then return false end + if not skinsData[model] then return warn("invalid ped model", 2) and false end + + return skinsData[model].groups +end + +function getPedModelWalkingStyle(model) + if not scheck("n") then return false end + if not skinsData[model] then return warn("invalid ped model", 2) and false end + + return skinsData[model].walking +end \ No newline at end of file diff --git a/helpers/stats.lua b/helpers/stats.lua new file mode 100644 index 0000000..b956196 --- /dev/null +++ b/helpers/stats.lua @@ -0,0 +1,86 @@ + +local path = "conf/stats.xml" +local statsData = {} + +local function loadData() + local fileNode = xmlLoadFile(path, true) + if not fileNode then return warn("skins conf file not found", 2) and false end + + local fileData = xmlNodeGetData(fileNode) + xmlUnloadFile(fileNode) + + for ig, groupData in ipairs(fileData.children) do + for is, statData in ipairs(groupData.children) do + local id = tonumber(statData.attributes.id) + statsData[id] = { + name = statData.attributes.name, + shortname = statData.attributes.shortname, + group = groupData.attributes.name + } + end + end + return true +end +loadData() + +function isValidPedStat(id) + if not scheck("n") then return false end + + return statsData[id] and true or false +end + +function getValidPedStats() + + local stats = {} + for id in pairs(statsData) do + stats[#stats + 1] = id + end + table.sort(stats) + + return stats +end + +function getValidPedStatsByGroup(group) + if not scheck("s") then return false end + + group = utf8.lower(group) + + local stats = {} + for id, data in pairs(statsData) do + if utf8.lower(data.group) == group then stats[#stats + 1] = id end + end + table.sort(stats) + + return stats +end + +function getPedStatName(id) + if not scheck("n") then return false end + if not statsData[id] then return warn("invalid ped stat", 2) and false end + + return statsData[id].name +end + +function getPedStatShortName(id) + if not scheck("n") then return false end + if not statsData[id] then return warn("invalid ped stat", 2) and false end + + return statsData[id].shortname +end + +function getPedStatGroup(id) + if not scheck("n") then return false end + if not statsData[id] then return warn("invalid ped stat", 2) and false end + + return statsData[id].group +end + +function getPedStatFromPartialName(partial) + if not scheck("s") then return false end + + partial = utf8.lower(partial) + for id, data in pairs(statsData) do + if utf8.find(utf8.lower(data.shortname), partial, 1, true) then return id end + end + return nil +end \ No newline at end of file diff --git a/helpers/time.lua b/helpers/time.lua new file mode 100644 index 0000000..16615db --- /dev/null +++ b/helpers/time.lua @@ -0,0 +1,79 @@ + +local PARSE_DATE_FORMAT = { + {"year", "Y", 1970, 9999}, + {"month", "M", 1, 12}, + {"day", "D", 1, 31}, + {"hour", "h", 0, 23}, + {"min", "m", 0, 59}, + {"sec", "s", 0, 59} +} + +function parseDuration(str) + if not scheck("s") then return false end + + str = str + :lower() + :gsub("permanent", "perm") + :gsub("mseconds?", "ms") + :gsub("msecs?", "ms") + :gsub("seconds?", "s") + :gsub("secs?", "s") + :gsub("minutes?", "m") + :gsub("mins?", "m") + :gsub("hours?", "h") + :gsub("days?", "d") + :gsub("weeks?", "w") + + if utf8.match(str, "perm") then return 0 end + + str = utf8.gsub(str, "%s", "") + if not utf8.match(str, "^%d+[%dsmhdw]*[smhdw]$") then return false end + + local seconds = 0 + local sbyt = {ms = 0.001, s = 1, m = 60, h = 60*60, d = 60*60*24, w = 60*60*24*7} + for n, t in utf8.gmatch(str, "(%d+)([smhdw]+)") do + seconds = seconds + tonumber(n) * sbyt[t] + end + return seconds +end + +function formatDuration(seconds, simple) + if not scheck("n,?b") then return false end + + if seconds == 0 then return "permanent" end + + seconds = math.round(math.abs(seconds), 3) + + local result = "" + local ts = { {"day", 60*60*24}, {"hour", 60*60}, {"min", 60}, {"sec", 1}, {"msec", 0.001} } + for _, data in ipairs(ts) do + local t = math.floor(seconds/data[2]) + if t > 0 then + seconds = math.round(seconds % data[2], 3) + result = result..t.." "..data[1]..(t > 1 and "s" or "").." " + if simple then break end + end + end + + return utf8.trim(result) +end + +function parseDate(str) + if not scheck("s") then return false end + + local current = os.date("*t") + + local date = {} + local found = false + for i, data in ipairs(PARSE_DATE_FORMAT) do + local n = utf8.match(str, data[2].."(%d+)") + if n then + date[data[1]] = math.clamp(tonumber(n), data[3], data[4]) + found = true + else + date[data[1]] = found and data[3] or current[data[1]] + end + end + + return os.time(date) +end \ No newline at end of file diff --git a/helpers/upgrades.lua b/helpers/upgrades.lua new file mode 100644 index 0000000..04bae50 --- /dev/null +++ b/helpers/upgrades.lua @@ -0,0 +1,55 @@ + +local path = "conf/upgrades.xml" +local upgradesData = {} + +local function loadData() + local fileNode = xmlLoadFile(path, true) + if not fileNode then return warn("vehicle upgrades conf file not found", 2) and false end + + local fileData = xmlNodeGetData(fileNode) + xmlUnloadFile(fileNode) + + for i, upgradeData in ipairs(fileData.children) do + local id = tonumber(upgradeData.attributes.id) + upgradesData[id] = upgradeData.attributes.name + end + return true +end +loadData() + +function getValidVehicleUpgrades() + local ids = {} + for id in pairs(upgradesData) do + table.insert(ids, id) + end + return table.sort(ids) +end + +function getValidVehicleUpgradeSlots() + + local slots = {} + for i = 1, 17 do + slots[i] = i - 1 + end + return slots +end + +function isValidVehicleUpgrade(id) + if not scheck("n") then return false end + + return upgradesData[id] and true or false +end + +function getVehicleUpgradeName(id) + if not scheck("n") then return false end + if not upgradesData[id] then return warn("invalid vehicle upgrade", 2) and false end + + return upgradesData[id] +end + +function getVehicleUpgradeFullName(id) + if not scheck("n") then return false end + if not upgradesData[id] then return warn("invalid vehicle upgrade", 2) and false end + + return getVehicleUpgradeSlotName(id).." "..upgradesData[id] +end \ No newline at end of file diff --git a/helpers/utils.lua b/helpers/utils.lua new file mode 100644 index 0000000..c215ab9 --- /dev/null +++ b/helpers/utils.lua @@ -0,0 +1,41 @@ + +local _toJSON = toJSON +function toJSON(value, ...) + + if value == nil then return "[ nil ]" end + + return _toJSON(value, ...) +end + +local _fromJSON = fromJSON +function fromJSON(s) + if not scheck("s") then return false end + + if s == "[ ]" then return end + if s == "[ nil ]" then return nil end + + return _fromJSON(s) +end + +function stripColorCodes(str) + if not scheck("s") then return false end + + str = utf8.gsub(str, "#%x%x%x%x%x%x", "") + return str +end + +function isInsideArea(x, y, minX, minY, maxX, maxY) + if not scheck("n[6]") then return false end + + if x < minX or x > maxX or + y < minY or y > maxY then + return false + end + return true +end + +function isInsideRectangle(x, y, rx, ry, width, height) + if not scheck("n[6]") then return false end + + return isInsideArea(x, y, rx, ry, rx + width, ry + height) +end \ No newline at end of file diff --git a/helpers/vehicle.lua b/helpers/vehicle.lua new file mode 100644 index 0000000..1043d3d --- /dev/null +++ b/helpers/vehicle.lua @@ -0,0 +1,111 @@ + +local validVehicleModels = {} +local invalidVehicleModels = {435, 449, 450, 537, 538, 569, 570, 584, 590, 591, 606, 607, 608} +for model = 400, 609 do + if not table.index(invalidVehicleModels, model) then + table.insert(validVehicleModels, model) + end +end + +local compatibleVehiclePaintjobs = { + [483] = {0}, -- camper + [534] = {0,1,2}, -- remington + [535] = {0,1,2}, -- slamvan + [536] = {0,1,2}, -- blade + [558] = {0,1,2}, -- uranus + [559] = {0,1,2}, -- jester + [560] = {0,1,2}, -- sultan + [561] = {0,1,2}, -- stratum + [562] = {0,1,2}, -- elegy + [565] = {0,1,2}, -- flash + [567] = {0,1,2}, -- savanna + [575] = {0,1}, -- broadway + [576] = {0,1,2} -- tornado +} + +function getValidVehicleModels() + return table.copy(validVehicleModels) +end + +function isValidVehicleModel(model) + if not scheck("n") then return false end + + return table.index(validVehicleModels, model) and true or false +end + +function getVehicleModelFromPartialName(partialName) + if not scheck("s") then return false end + + local model = getVehicleModelFromName(partialName) + if model then return model end + + partialName = utf8.lower(partialName) + for i, model in ipairs(validVehicleModels) do + if utf8.find(utf8.lower(getVehicleNameFromModel(model)), partialName, 1, true) then return model end + end + return nil +end + +function getVehicleCompatiblePaintjobs(model) + if not scheck("n|u:element:vehicle") then return false end + if type(model) == "number" and not isValidVehicleModel(model) then return warn("invalid vehicle model", 2) and false end + + if type(model) == "userdata" then model = getElementModel(model) end + + return compatibleVehiclePaintjobs[model] or {} +end + +function isVehicleUpgradeCompatible(model, upgrade) + if not scheck("n|u:element:vehicle,n") then return false end + if type(model) == "number" and not isValidVehicleModel(model) then return warn("invalid vehicle model", 2) and false end + + if type(model) == "userdata" then model = getElementModel(model) end + + return table.index(getVehicleCompatibleUpgrades(model), upgrade) and true or false +end + +function getVehicleOneColor(vehicle, number) + if not scheck("u:element:vehicle,n") then return false end + if not (number >= 1 and number <= 4) then return warn("invalid vehicle color number", 2) and false end + + local colors = {getVehicleColor(vehicle, true)} + return colors[(number-1)*3+1], colors[(number-1)*3+2], colors[(number-1)*3+3] +end + +function setVehicleOneColor(vehicle, number, r, g, b) + if not scheck("u:element:vehicle,n[4]") then return false end + if not (number >= 1 and number <= 4) then return warn("invalid vehicle color number", 2) and false end + + local colors = {getVehicleColor(vehicle, true)} + colors[(number-1)*3+1] = r + colors[(number-1)*3+2] = g + colors[(number-1)*3+3] = b + + return setVehicleColor(vehicle, unpack(colors)) +end + +function getVehicleFreePassengerSeat(vehicle) + if not scheck("u:element:vehicle") then return false end + + local lastSeat = getVehicleMaxPassengers(vehicle) + if lastSeat == 0 then return nil end + + local seatsState = {} + for seat, occupant in pairs(getVehicleOccupants(vehicle)) do + seatsState[seat] = true + end + for seat = 1, lastSeat do + if not seatsState[seat] then return seat end + end + return nil +end + +function getVehicleOneOccupant(vehicle) + if not scheck("u:element:vehicle") then return false end + + for i = 0, getVehicleMaxPassengers(vehicle) do + local occupant = getVehicleOccupant(vehicle, i) + if occupant then return occupant end + end + return nil +end \ No newline at end of file diff --git a/helpers/walkingstyles.lua b/helpers/walkingstyles.lua new file mode 100644 index 0000000..aa56607 --- /dev/null +++ b/helpers/walkingstyles.lua @@ -0,0 +1,76 @@ + +local styles = { + [0] = "Default", + [54] = "Carl Johnson", + [55] = "Fat Carl Johnson", + [56] = "Muscle Carl Johnson", + [57] = "Carl Johnson (Rocket launcher)", + [58] = "Fat Carl Johnson (Rocket launcher)", + [59] = "Muscle Carl Johnson (Rocket launcher)", + [60] = "Carl Johnson (Armed)", + [61] = "Fat Carl Johnson (Armed)", + [62] = "Muscle Carl Johnson (Armed)", + [63] = "Carl Johnson (Bat)", + [64] = "Fat Carl Johnson (Bat)", + [65] = "Muscle Carl Johnson (Bat)", + [66] = "Carl Johnson (Chainsaw)", + [67] = "Fat Carl Johnson (Chainsaw)", + [68] = "Muscle Carl Johnson (Chainsaw)", + [69] = "Sneaking", + [70] = "JetPack", + [118] = "Man", + [119] = "Shuffle", + [120] = "Old man", + [121] = "Gang 1", + [122] = "Gang 2", + [123] = "Old fat man", + [124] = "Fat man", + [125] = "Jogger", + [126] = "Drunk man", + [127] = "Blind man", + [128] = "SWAT", + [129] = "Woman", + [130] = "Shopping", + [131] = "Busy woman", + [132] = "Sexy woman", + [133] = "Pro", + [134] = "Old woman", + [135] = "Fat woman", + [136] = "Jog woman", + [137] = "Old fat woman", + [138] = "Skates", +} + +function getValidWalkingStyles() + + local list = {} + for style in pairs(styles) do + table.insert(list, style) + end + table.sort(list) + + return list +end + +function isValidWalkingStyle(style) + if not scheck("n") then return false end + + return styles[style] and true or false +end + +function getWalkingStyleName(style) + if not scheck("n") then return false end + if not styles[style] then return warn("invalid walking style", 2) and false end + + return styles[style] +end + +function getWalkingStyleFromPartialName(partialName) + if not scheck("s") then return false end + + partialName = utf8.lower(partialName) + for style, name in pairs(styles) do + if utf8.find(utf8.lower(name), partialName, 1, true) then return style end + end + return nil +end \ No newline at end of file diff --git a/helpers/weapon.lua b/helpers/weapon.lua new file mode 100644 index 0000000..8b2da3b --- /dev/null +++ b/helpers/weapon.lua @@ -0,0 +1,37 @@ + +local validWeaponIDs = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, + 22, 23, 24, + 25, 26, 27, + 28, 29, 32, + 30, 31, + 33, 34, + 35, 36, 37, 38, + 16, 17, 18, 39, + 41, 42, 43, + 10, 11, 12, 14, 15, + 44, 45, 46 +} + +function getValidWeaponIDs() + return table.copy(validWeaponIDs) +end + +function isValidWeaponID(id) + if not scheck("n") then return false end + + return table.index(validWeaponIDs, id) and true or false +end + +function getWeaponIDFromPartialName(partialName) + if not scheck("s") then return false end + + local id = getWeaponIDFromName(partialName) + if id then return id end + + partialName = utf8.lower(partialName) + for i, id in ipairs(validWeaponIDs) do + if utf8.find(utf8.lower(getWeaponNameFromID(id)), partialName, 1, true) then return id end + end + return nil +end \ No newline at end of file diff --git a/helpers/weathers.lua b/helpers/weathers.lua new file mode 100644 index 0000000..ec96cc4 --- /dev/null +++ b/helpers/weathers.lua @@ -0,0 +1,53 @@ + +local path = "conf/weathers.xml" +local weathersData = {} + +local function loadData() + local fileNode = xmlLoadFile(path, true) + if not fileNode then return warn("weathers conf file not found", 2) and false end + + local fileData = xmlNodeGetData(fileNode) + xmlUnloadFile(fileNode) + + for i, weatherData in ipairs(fileData.children) do + local id = tonumber(weatherData.attributes.id) + weathersData[id] = weatherData.attributes.name + end + return true +end +loadData() + +function getValidWeathers() + + local weathers = {} + for i = 0, 255 do + weathers[i+1] = i + end + return weathers +end + +function isValidWeather(id) + if not scheck("n") then return false end + + if math.floor(id) ~= id then return false end + + return id >= 0 and id <= 255 +end + +function getWeatherName(id) + if not scheck("n") then return false end + if not isValidWeather(id) then return warn("invalid weather", 2) and false end + + return weathersData[id] or "Unknown" +end + +function getWeatherFromPartialName(partialName) + if not scheck("s") then return false end + + partialName = utf8.lower(partialName) + for id, name in pairs(weathersData) do + if utf8.find(utf8.lower(name), partialName, 1, true) then return id end + end + return nil +end + diff --git a/helpers/wsp.lua b/helpers/wsp.lua new file mode 100644 index 0000000..8400cc7 --- /dev/null +++ b/helpers/wsp.lua @@ -0,0 +1,48 @@ + +local wspsData = { + {nid = "hovercars", name = "Hover Cars"}, + {nid = "aircars", name = "Air Cars"}, + {nid = "extrabunny", name = "Extra Bunny"}, + {nid = "extrajump", name = "Extra Jump"}, + {nid = "randomfoliage", name = "Random Foliage", default = true}, + {nid = "snipermoon", name = "Sniper Moon"}, + {nid = "extraairresistance", name = "Extra Air Resistance", default = true}, + {nid = "underworldwarp", name = "Under World Warp", default = true}, +} + +function getValidWorldSpecialProperties() + local list = {} + for i, data in ipairs(wspsData) do + table.insert(list, data.nid) + end + return list +end + +function isValidWorldSpecialProperty(property) + if not scheck("s") then return false end + + for i, data in ipairs(wspsData) do + if data.nid == property then return true end + end + return false +end + +function getWorldSpecialPropertyName(property) + if not scheck("s") then return false end + + for i, data in ipairs(wspsData) do + if data.nid == property then return data.name end + end + + return warn("invalid world special property", 2) and false +end + +function getWorldSpecialPropertyDefault(property) + if not scheck("s") then return false end + + for i, data in ipairs(wspsData) do + if data.nid == property then return data.default == true end + end + + return warn("invalid world special property", 2) and false +end \ No newline at end of file diff --git a/helpers/xml.lua b/helpers/xml.lua new file mode 100644 index 0000000..d1baf0d --- /dev/null +++ b/helpers/xml.lua @@ -0,0 +1,28 @@ + +function xmlNodeGetData(node) + if not scheck("u:xml-node") then return false end + + local data = {} + data.name = xmlNodeGetName(node) + data.value = xmlNodeGetValue(node) + data.attributes = xmlNodeGetAttributes(node) + data.children = {} + + local children = xmlNodeGetChildren(node) + if #children == 0 then return data end + + for i, childNode in ipairs(children) do + data.children[i] = xmlNodeGetData(childNode) + end + + return data +end + +function xmlFindChildByAttribute(node, name, attribute, value) + if not scheck("u:xml-node,s[3]") then return false end + + for i, child in ipairs(xmlNodeGetChildren(node)) do + if xmlNodeGetName(child) == name and xmlNodeGetAttribute(child, attribute) == value then return child end + end + return nil +end \ No newline at end of file diff --git a/libs/check.lua b/libs/check.lua new file mode 100644 index 0000000..484c5b7 --- /dev/null +++ b/libs/check.lua @@ -0,0 +1,183 @@ + +checkers = {} + +local string_match = string.match +local string_format = string.format +local string_gsub = string.gsub +local string_find = string.find +local table_concat = table.concat +local debug_getinfo = debug.getinfo +local debug_getlocal = debug.getlocal + +local _string_rep = string.rep +local function string_rep(s, n, sep) + if n == 1 then return s end + if n < 1 then return "" end + + return _string_rep(s..(sep or ""), n - 1)..s +end + +local function mta_type(value) + + local t = type(value) + if t ~= "userdata" then return t end + + local udt = getUserdataType(value) + if udt == t then return t end + if udt ~= "element" then return t..":"..udt end + + return t..":"..udt..":"..getElementType(value) +end + +local function is_subtype(sub, parent) + + return + sub == parent or + string_find(sub, parent..":", 1, true) == 1 +end + +local default_checkers = { + ["byte"] = function(v) return type(v) == "number" and v >= 0 and v <= 255 end, + ["userdata:element:gui"] = function(v) return string_match(mta_type(v), "^userdata:element:gui%-") end +} + +local type_cuts = { + ["b"] = "boolean", + ["n"] = "number", + ["s"] = "string", + ["t"] = "table", + ["u"] = "userdata", + ["f"] = "function", + ["th"] = "thread" +} + +local cache = {} + +local function parse(pattern) + + if cache[pattern] then return cache[pattern] end + + local result = pattern + result = string_gsub(result, "(%a+)", type_cuts) + result = string_gsub(result, "(%?)(%a+)", "nil|%2") + result = string_gsub(result, "%?", "any") + result = string_gsub(result, "!", "notnil") + result = string_gsub(result, "([^,]+)%[(%d)%]", function(t, n) return string_rep(t, tonumber(n), ",") end) + + result = split(result, ",") + for i = 1, #result do + result[i] = split(result[i], "|") + end + + cache[pattern] = result + + return result +end + +local function arg_invalid_msg(funcName, argNum, argName, msg) + + msg = msg and string_format(" (%s)", msg) or "" + + return string_format( + "bad argument #%d '%s' to '%s'%s", + argNum, argName or "?", funcName or "?", msg + ) +end + +local function expected_msg(variants, found) + + local expected = {} + for i = 1, #variants do + expected[i] = string_gsub(variants[i], ".+:", "") + end + expected = table_concat(expected, "\\") + found = string_gsub(found, ".+:", "") + + return string_format( + "%s expected, got %s", + expected, found + ) +end + +function warn(msg, lvl) + check("s,?n") + + lvl = (lvl or 1) + 1 + local dbInfo = debug_getinfo(lvl, "lS") + + if dbInfo and lvl > 1 then + local src = dbInfo.short_src + local line = dbInfo.currentline + + msg = string_format( + "%s:%s: %s", + src, line, msg + ) + end + + return outputDebugString("WARNING: "..msg, 4, 255, 127, 0) +end + +local function check_one(variants, value) + + local valueType = mta_type(value) + local mt = getmetatable(value) + local valueClass = mt and mt.__type + + for i = 1, #variants do + + local variant = variants[i] + + if variant == "any" then return true end + if variant == "notnil" and value ~= nil then return true end + if valueClass and valueClass == variant then return true end + + if is_subtype(valueType, variant) then return true end + + local checker = default_checkers[variant] + if checker and checker(value) then return true end + + checker = checkers[variant] + if type(checker) == "function" and checker(value) then return true end + end + + local msg = expected_msg(variants, valueClass or valueType) + return false, msg +end + +local function check_main(pattern) + + local parsed = parse(pattern) + for argNum = 1, #parsed do + + local argName, value = debug_getlocal(3, argNum) + local success, descMsg = check_one(parsed[argNum], value) + if not success then + + local funcName = debug_getinfo(3, "n").name + local msg = arg_invalid_msg(funcName, argNum, argName, descMsg) + return false, msg + end + end + + return true +end + +function check(pattern) + if type(pattern) ~= "string" then check("string") end + + local success, msg = check_main(pattern) + if not success then error(msg, 3) end + + return true +end + +function scheck(pattern) + if type(pattern) ~= "string" then check("string") end + + local success, msg = check_main(pattern) + --if not success then return warn(msg, 3) and false end + if not success then error(msg, 3) end + + return true +end diff --git a/libs/coroutine.lua b/libs/coroutine.lua new file mode 100644 index 0000000..679b02f --- /dev/null +++ b/libs/coroutine.lua @@ -0,0 +1,17 @@ + +function coroutine.sleep(ms) + check("n") + if ms < 0 then error("negative interval", 2) end + + local c = coroutine.running() + if not c then error("invalid thread", 2) end + + setTimer( + function() + if coroutine.status(c) ~= "suspended" then return end + coroutine.resume(c) + end, ms, 1 + ) + + return coroutine.yield() +end \ No newline at end of file diff --git a/libs/enum.lua b/libs/enum.lua new file mode 100644 index 0000000..8a118ab --- /dev/null +++ b/libs/enum.lua @@ -0,0 +1,12 @@ + +function enum(args, prefix) + check("t,?s") + + for i, targ in ipairs(args) do + if prefix then + _G[targ] = prefix..i + else + _G[targ] = i + end + end +end \ No newline at end of file diff --git a/libs/math.lua b/libs/math.lua new file mode 100644 index 0000000..bccc991 --- /dev/null +++ b/libs/math.lua @@ -0,0 +1,15 @@ + +function math.clamp(x, min, max) + check("n,?n[2]") + + return min and x < min and min or max and x > max and max or x +end + +function math.round(x, decimals) + check("n,?n") + + local mult = 10^(decimals or 0) + x = x * mult + + return (x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5))/mult +end \ No newline at end of file diff --git a/libs/string.lua b/libs/string.lua new file mode 100644 index 0000000..158c8f9 --- /dev/null +++ b/libs/string.lua @@ -0,0 +1,134 @@ + +function utf8.literalize(s) + check("s") + + return utf8.gsub(s, "[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%0") +end + +local _rep = string.rep +function string.rep(s, n, sep) + check("s,n,?s") + + return + n < 1 and "" or + n < 2 and s or + _rep(s..(sep or ""), n - 1)..s +end + +function utf8.split(s, sep, limit, plain) + check("s,?s,?n,?b") + + if limit and limit < 1 then return {} end + if s == "" then return {""} end + + sep = sep or "" + sep = plain and utf8.literalize(sep) or sep + + local t = {} + local i = 1 + for v in utf8.gmatch(s, sep == "" and "." or "([^"..sep.."]+)") do + t[i] = v + if limit and i == limit then break end + i = i + 1 + end + + return t +end + +function utf8.trim(s) + check("s") + + s = utf8.gsub(s, "^%s*(.-)%s*$", "%1") + return s +end + +function utf8.upperf(s) + check("s") + + s = utf8.gsub(s, "^%l", utf8.upper) + return s +end + +local _find = utf8.find +function utf8.find(s, pattern, init, plain, soft) + check("s[2],?n,?b[2]") + + if soft then + s = utf8.lower(utf8.trim(s)) + pattern = utf8.lower(utf8.trim(pattern)) + end + + return _find(s, pattern, init, plain) +end + +function utf8.insert(str1, str2, pos) + check("s[2],?n") + + pos = pos or (#str1 + 1) + + return utf8.sub(str1, 0, pos - 1)..str2..utf8.sub(str1, pos) +end + +-- https://gist.github.com/Badgerati/3261142 +function utf8.levenshtein(str1, str2) + check("s[2]") + + if str1 == str2 then return 0 end + + local len1 = utf8.len(str1) + local len2 = utf8.len(str2) + if len1 == 0 then return len2 end + if len2 == 0 then return len1 end + + local matrix = {} + for i = 0, len1 do + matrix[i] = {} + matrix[i][0] = i + end + for j = 0, len2 do + matrix[0][j] = j + end + + local cost = 0 + for i = 1, len1 do + for j = 1, len2 do + if str1:byte(i) == str2:byte(j) then + cost = 0 + else + cost = 1 + end + matrix[i][j] = math.min(matrix[i-1][j] + 1, matrix[i][j-1] + 1, matrix[i-1][j-1] + cost) + end + end + + return matrix[len1][len2] +end + +function utf8.totype(s) + check("s") + + local value = tonil(s) + if value ~= false then return value end + + value = toboolean(s) + if value ~= nil then return value end + + value = tonumber(s) + if value then return value end + + return s +end + +local charset = utf8.split("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") +function utf8.random(length) + check("n") + + length = math.floor(length) + if length < 1 then return "" end + + local str = "" + for i = 1, length do + str = str..charset[math.random(#charset)] + end + return str +end \ No newline at end of file diff --git a/libs/table.lua b/libs/table.lua new file mode 100644 index 0000000..2b7b53c --- /dev/null +++ b/libs/table.lua @@ -0,0 +1,107 @@ + +function table.unpack(t) + check("t") + + return unpack(t, nil, t.n) +end + +function table.pack(...) + return {n = select("#", ...), ...} +end + +function table.index(t, v) + check("t,!") + + local size = #t + if size == 0 then return nil end + + for i = 1, size do + if t[i] == v then return i end + end + return nil +end + +function table.key(t, v) + check("t,!") + + for k, tv in pairs(t) do + if tv == v then return k end + end + return nil +end + +function table.reverse(t) + check("t") + + for i = 1, math.floor(#t / 2) do + t[i], t[#t - i + 1] = t[#t - i + 1], t[i] + end + return t +end + +function table.removevalue(t, v) + check("t") + + if v == nil then return false end + + local size = #t + if size == 0 then return nil end + + for i = 1, size do + if t[i] == v then return table.remove(t, i) end + end + return false +end + +function table.equal(t1, t2) + if type(t1) ~= type(t2) then return false end + if type(t1) ~= "table" then return t1 == t2 end + + for k, v in pairs(t1) do + if not table.equal(v, t2[k]) then return false end + end + for k, v in pairs(t2) do + if not table.equal(v, t1[k]) then return false end + end + + return true +end + +function table.copy(t) + check("t") + + local result = {} + for k, v in pairs(t) do + result[k] = v + end + return result +end + +function table.copydeep(t) + if type(t) ~= "table" then return t end + + local result = {} + for k, v in pairs(t) do + result[k] = table.copydeep(v) + end + return result +end + +function table.merge(t, t2) + check("t[2]") + + for k, v in pairs(t2) do + t[k] = v + end + return t +end + +function table.size(t) + check("t") + + local size = 0 + for _ in pairs(t) do + size = size + 1 + end + return size +end \ No newline at end of file diff --git a/libs/utils.lua b/libs/utils.lua new file mode 100644 index 0000000..8897ceb --- /dev/null +++ b/libs/utils.lua @@ -0,0 +1,24 @@ + +function address(val) + check("t|u|f|th") + + return utf8.match(tostring(val), "^.+:%s(%x+)$") +end + +function toboolean(v) + check("b|s") + + if v == false or v == true then return v end + if v == "true" then return true end + if v == "false" then return false end + + return nil +end + +function tonil(v) + check("?s") + + if v == nil then return nil end + + return v == "nil" and nil or false +end \ No newline at end of file diff --git a/meta.xml b/meta.xml new file mode 100644 index 0000000..c7cb826 --- /dev/null +++ b/meta.xml @@ -0,0 +1,599 @@ + + + + + + + + + + + + + + + +