Compare commits
905 commits
Author | SHA1 | Date | |
---|---|---|---|
|
27e3efca97 | ||
|
8bd1a9b77d | ||
|
bb8c4bce82 | ||
|
6902ecaa7c | ||
|
dc08b91314 | ||
|
822dd19393 | ||
6cd0234a5f | |||
|
19e7f57284 | ||
|
5d1401cdf6 | ||
|
91af86cca2 | ||
|
c935149c1e | ||
|
e5bda57665 | ||
|
ef294f207c | ||
|
f89d02a60d | ||
|
d090cd2420 | ||
|
99c17795c1 | ||
|
138221b93e | ||
|
8abfc21181 | ||
309c461e11 | |||
|
93fa14d832 | ||
df4fb9be91 | |||
eaac9ff53a | |||
|
0d2f5539b6 | ||
|
0f4cac29aa | ||
|
010cd36375 | ||
|
c17da72815 | ||
|
ff83c401b8 | ||
|
a007fa94a6 | ||
|
1d4d1635eb | ||
|
56f08a88aa | ||
|
833d0e244b | ||
|
3e09807638 | ||
|
ea7013322f | ||
|
98b31b9f67 | ||
|
8364a8e4de | ||
|
473ad80e8f | ||
|
180d20ce4a | ||
|
b585234b08 | ||
|
5379c79488 | ||
|
8aefe88104 | ||
|
cb552af660 | ||
|
95c2ca0965 | ||
|
5167a71e6f | ||
|
eac34d2e7e | ||
|
ff80981282 | ||
|
45493dc23b | ||
|
359f28133f | ||
|
c7cbda98fe | ||
|
a7ac864ca5 | ||
|
9993e03f04 | ||
|
997653eb4a | ||
|
f94fbc685a | ||
|
e2e8beb45c | ||
6b38a21454 | |||
|
430d5683f0 | ||
|
59a7955132 | ||
|
1fb1d3e7b5 | ||
|
0efc2c565b | ||
|
c94ab0cde1 | ||
|
d3b9a4b9e8 | ||
|
3cf2be51bc | ||
|
a3f0f279a8 | ||
|
a94c9d6d54 | ||
|
63493e78de | ||
|
8cb7b8a13a | ||
|
9d9f1685d8 | ||
|
91ac6b00c1 | ||
|
2a85532448 | ||
|
5a292aabc5 | ||
|
cf513c6724 | ||
|
b628601cc8 | ||
|
649bdc4c31 | ||
|
3030158159 | ||
|
23456ae041 | ||
|
4f6f1835d2 | ||
|
636d0bf277 | ||
|
adc74f47c0 | ||
|
1fd17ca0b3 | ||
|
9498620e03 | ||
|
0850f41617 | ||
|
f2c40a4bd5 | ||
|
bfb89dec01 | ||
|
395c36fa8b | ||
|
7e784ba361 | ||
|
95b81d65fe | ||
|
43de7b277a | ||
|
1d0bab5e65 | ||
|
e0f065b678 | ||
|
5b5c46f802 | ||
|
489c8a6609 | ||
|
a13b17fee6 | ||
|
e0b9ceb86d | ||
|
6bc1fa2343 | ||
|
763b3ff6a7 | ||
|
816b6654da | ||
|
efcc3c6028 | ||
|
de71558c5d | ||
|
3f53fc983c | ||
|
089ecb3a4c | ||
|
7a46f12c02 | ||
|
96eed54a65 | ||
|
abe7217a58 | ||
|
54081c1b0f | ||
|
12f5824da9 | ||
|
e11302766b | ||
|
70f0c55d35 | ||
|
476125cc74 | ||
|
b8196d6284 | ||
|
a11c1558ed | ||
|
cec77a9bd9 | ||
|
0685e62a8f | ||
|
53463641b8 | ||
66ed1a0b72 | |||
f830ce7fad | |||
7fb48dfa1f | |||
|
fa7f6c6f51 | ||
|
753897222a | ||
|
4a28499aaa | ||
|
03c773fd79 | ||
fe55442dac | |||
|
34fc23d024 | ||
|
fcfb5ed3a8 | ||
|
f36ec1483a | ||
|
aa04fb345c | ||
|
3f79756f70 | ||
|
8220216893 | ||
0d302cf0c1 | |||
|
1e3a604d0f | ||
|
a65ef2ae1b | ||
|
94ba331ed0 | ||
|
59aa780654 | ||
|
0116d405b2 | ||
|
9f58414e99 | ||
|
a52dfc1483 | ||
|
65413e1c94 | ||
|
edeb4003fd | ||
|
4844fa1ead | ||
|
cd34dcb0dd | ||
1f211ca0ab | |||
20b9fce7df | |||
|
baa03424d4 | ||
|
347da823b7 | ||
|
fe4ba1db02 | ||
|
3c68d8e314 | ||
|
1b25ed2904 | ||
|
1ab6a7a9fd | ||
|
474ceb139a | ||
|
b322d3dfa5 | ||
|
7b662801f2 | ||
|
ce21063d7c | ||
|
7b4942fc3d | ||
|
b07202a64b | ||
|
0392c163a0 | ||
|
e5e83d1ad6 | ||
|
ef9550ced5 | ||
|
d7c603e881 | ||
|
3565c897c0 | ||
|
9c27057bf3 | ||
6f8b1e4896 | |||
|
5040deddb6 | ||
|
c9ce29741b | ||
|
cd78fea1f5 | ||
|
0e54d7f124 | ||
|
2511bb00a7 | ||
|
39d9557559 | ||
|
507d88e63e | ||
|
779a11c85a | ||
|
f29636b038 | ||
|
f860821498 | ||
|
e3981c8db2 | ||
|
1c814285c6 | ||
|
56949aad09 | ||
|
7e9aaeafe6 | ||
|
4e68c64daf | ||
|
37afe7b05e | ||
ec6060a6cb | |||
|
e16c28fd40 | ||
|
1aa6aef679 | ||
|
9fe3b02d71 | ||
|
02d812f103 | ||
|
ed07ba8613 | ||
|
ede3365f64 | ||
|
e92cb4ee64 | ||
|
dc7e3888d7 | ||
|
eb1bddf44e | ||
8865692f1d | |||
|
c43648faec | ||
|
74d7f3d66f | ||
|
fda0e9ab5e | ||
|
96adf83fad | ||
|
e4850666c5 | ||
|
462e9a4f5a | ||
|
414d8ff8f3 | ||
|
f620f453f2 | ||
|
bb23d0a9cd | ||
|
5efc8da300 | ||
|
3ddd8b4676 | ||
|
bdec11996d | ||
|
165987d60b | ||
|
3b9830336e | ||
|
de0a570a40 | ||
|
dd1db5dcab | ||
|
5f7bf6d3f2 | ||
|
854c3c6f71 | ||
|
3df7750b54 | ||
|
8ddafdf996 | ||
|
2b222d8e2a | ||
|
b1ee221555 | ||
|
83ac971792 | ||
|
8c853fb99e | ||
|
bfcd78c43e | ||
|
2e53cdcad7 | ||
|
fbee7c85bd | ||
|
a86a62ae1f | ||
|
2963751f6c | ||
|
1dfffdc31b | ||
|
015908a3b0 | ||
|
28b5b6fcaa | ||
|
be6b2c8a71 | ||
|
ac186190e5 | ||
|
0cd23eecfa | ||
|
278057dddf | ||
|
5a6a8a0931 | ||
|
2e9ff358cc | ||
|
fad48b9603 | ||
|
7a05563f0f | ||
|
67fe1d3e4d | ||
|
210b64a86d | ||
40aeed70af | |||
|
821c66c5a2 | ||
|
5d49cf463d | ||
|
79ca711d68 | ||
|
a9b60db54f | ||
|
505f06d973 | ||
|
6a28ae21d8 | ||
e2aef015fa | |||
|
3967c1ecbb | ||
|
1e6660ab9b | ||
|
37d6f1de2d | ||
|
a120963451 | ||
|
ccabfa335a | ||
|
75549a0b1c | ||
|
a3feece9d4 | ||
|
066df67ffa | ||
|
b79d3ce4e2 | ||
|
bcfc7d2f55 | ||
|
603cc10b42 | ||
|
ab3c832960 | ||
|
64b152e34b | ||
|
f3afbcadd0 | ||
|
de0f017540 | ||
|
57e2072bc0 | ||
|
c1876392bc | ||
|
52a6c4f4da | ||
|
83db30496d | ||
|
5ae8f662b9 | ||
|
fc90630781 | ||
|
3450d0ed4f | ||
|
b18e1ed519 | ||
|
58d64fc0c8 | ||
|
1cb8ea99a3 | ||
|
e5ed668a33 | ||
|
38a91a3dc3 | ||
|
5309060af8 | ||
|
1f11ceb91a | ||
|
609eba08a8 | ||
|
851589c695 | ||
|
2f3fb07b7c | ||
|
b4b119cf42 | ||
|
acae6a6369 | ||
|
8e4fc4b65e | ||
|
30dc027e3e | ||
|
888f00dd8b | ||
|
dbf5c989c2 | ||
|
3f5371a7c1 | ||
|
70caf89295 | ||
|
49864eb162 | ||
|
d7404679c7 | ||
|
ecf5c9db44 | ||
|
5dd5fb2ad1 | ||
|
e3449e7fd9 | ||
|
6db01d7b94 | ||
|
1baf10fa87 | ||
|
02c9bce645 | ||
|
4412d02b34 | ||
|
9b4ca94c13 | ||
|
029873fd5f | ||
|
616846c316 | ||
|
ebdc8a6261 | ||
|
90fce384d0 | ||
|
3b0f6ebab2 | ||
|
59788d9116 | ||
|
59aef5c170 | ||
|
9bb3854554 | ||
|
a3b364ae23 | ||
|
f0099ac5e8 | ||
|
577f8d7928 | ||
|
dc6903cfae | ||
|
57677278c4 | ||
|
5905b2d6cf | ||
|
3cd7a8945c | ||
|
98bb475afe | ||
|
7e7cfd7cd0 | ||
|
a8db74d40d | ||
|
3ab15e63e8 | ||
|
ee21921601 | ||
|
4eb7d5ab8b | ||
|
38994b941c | ||
|
9182427a0a | ||
|
d6c31f67ba | ||
|
a91e24eb01 | ||
|
c484ae73d6 | ||
|
d40a7b6b4f | ||
|
8a2c5f65bb | ||
|
67e1d6c7fc | ||
|
780f155377 | ||
|
d6dbe5c861 | ||
|
73361d5a52 | ||
|
e4cf40789c | ||
|
0757d6f293 | ||
|
7a63987cbe | ||
|
24b6498774 | ||
|
2762a43dbb | ||
|
b69f824b57 | ||
|
4acc730d38 | ||
|
b1847bf84c | ||
|
2960f960de | ||
|
dcb989fd92 | ||
|
bcef44e286 | ||
|
7d73601a58 | ||
|
7fe43a969f | ||
|
0dbf466817 | ||
|
6e83958653 | ||
|
b4bbc0630a | ||
|
1f72033a6a | ||
|
e1fa89cf60 | ||
|
62e6130223 | ||
|
dc80b1c28f | ||
|
1b7287a742 | ||
|
ab386e000d | ||
|
6416ef5fb7 | ||
|
e08908e22e | ||
|
7c8af9730f | ||
|
0a5305b947 | ||
|
eafc42531f | ||
|
d1a2dc2fa6 | ||
|
4fa76aa27f | ||
|
6757e1a1a8 | ||
|
c80e504283 | ||
|
d5400f370f | ||
|
60026b1c85 | ||
|
2b855156bf | ||
|
d8f6703c70 | ||
bff6dffe90 | |||
|
74a41c1bf1 | ||
|
d87d3f6cee | ||
|
72adb2172f | ||
|
c86190def1 | ||
|
0ab32ffffd | ||
|
375f1b9334 | ||
|
02459483e8 | ||
|
768aadc225 | ||
|
0a8773dc04 | ||
|
e57b8c3b15 | ||
|
78c23507c9 | ||
|
2407800aca | ||
|
061742ed07 | ||
|
ee3d0ba502 | ||
|
036c171eb3 | ||
|
9f662f21f1 | ||
|
2867ccb9b8 | ||
|
fea97972a2 | ||
|
55a2338a3e | ||
|
483713e635 | ||
|
cacaa97b6e | ||
|
b4c3cc77ac | ||
|
9ff142e6e2 | ||
|
df8570b16e | ||
|
7a2f31f8db | ||
|
924832dede | ||
|
82e5844dad | ||
|
0869b16ba6 | ||
|
31e625aa71 | ||
|
eafb566e91 | ||
|
7b8335a704 | ||
|
4c876cb809 | ||
|
ed4d99fa6a | ||
|
103f7fecfb | ||
|
aafc1fb13a | ||
|
5876ce9f22 | ||
|
9a91bf999d | ||
|
57f730e788 | ||
|
4cea07675c | ||
|
857da2a9f3 | ||
|
a0f92227f3 | ||
|
623af1c312 | ||
|
8c14c3cda3 | ||
|
462ccb6d15 | ||
|
f4609ffba6 | ||
|
8853aad620 | ||
|
49ae85af03 | ||
|
02132a2eec | ||
|
b0f23a406b | ||
|
bca3de9454 | ||
|
2f464f846a | ||
|
bee8562d45 | ||
|
689994552f | ||
|
151cfe9b2f | ||
|
8eda0532fa | ||
|
fccfe8b5b0 | ||
|
7db61c9114 | ||
|
7237b7819a | ||
|
9e28822e8f | ||
|
b0861ce0fb | ||
|
63e2b926e6 | ||
|
33be2ed716 | ||
|
9adf296539 | ||
|
db4dbc6dcb | ||
|
fc107e64b8 | ||
|
1dc0ea67e3 | ||
|
9e67a8542f | ||
|
382c1733a6 | ||
|
6a4e44918c | ||
|
940834f70e | ||
|
533b12b11c | ||
|
7f64bd0cc3 | ||
|
2e68895aea | ||
|
66e122ea63 | ||
|
65f2319a64 | ||
|
594e773236 | ||
|
518e263c69 | ||
|
e0f5e65ff6 | ||
|
3b29d10443 | ||
|
ef5d06a169 | ||
|
a0f6b25b9e | ||
|
dfca4d3077 | ||
|
4cf8905768 | ||
|
aaa0977d54 | ||
|
2169acb3d2 | ||
|
f1c230d39b | ||
|
10be29c121 | ||
|
3bc0e129b3 | ||
|
52c6f21aba | ||
|
bcdc5f14a5 | ||
|
4d5bf6c2ae | ||
|
e2af4f51d2 | ||
|
1247196c35 | ||
|
63f623647b | ||
|
71cf5e7c96 | ||
|
5d72132ecc | ||
|
0773dc024a | ||
|
42ab2da1ed | ||
|
155ae550db | ||
|
422a097598 | ||
|
c063230f4c | ||
|
933cfa84c0 | ||
|
151d8cc945 | ||
|
65ea0edc12 | ||
|
048b444859 | ||
|
73efb5fc99 | ||
|
f1c62245da | ||
|
83303cf5e6 | ||
|
2297350c6e | ||
|
6628220429 | ||
|
b42c69f9c4 | ||
|
ad59ae01d4 | ||
|
e0ba2579d9 | ||
|
dc0d693d1a | ||
|
8619de8ba5 | ||
|
a0dcb85129 | ||
|
11dfc5b1e5 | ||
|
df1c790191 | ||
|
860c696212 | ||
|
1ad3ec45c7 | ||
|
2b94dbabe0 | ||
|
2931aa690c | ||
|
d2ed3bdd5d | ||
|
b6387422be | ||
|
80e47baded | ||
|
0db32ad4c4 | ||
|
75c79e15e8 | ||
|
93470b50af | ||
|
06051c0bbd | ||
|
f7d82f08b0 | ||
|
85bbcaabaa | ||
|
dfb0781f82 | ||
|
6ea8326939 | ||
|
66aa1fc9ea | ||
|
fbffda1e65 | ||
|
c5f18d1eef | ||
|
27f9f32269 | ||
|
8c0075d120 | ||
|
bf1df9bc90 | ||
|
8000ed6c8c | ||
|
cd8f4b77ab | ||
|
305144cf87 | ||
|
96c82535a4 | ||
|
c14a6f0ae7 | ||
|
3c204ee883 | ||
|
3071a1154c | ||
|
d0aa55ba04 | ||
|
2d3389dd32 | ||
|
6f1081fe4f | ||
|
75b08abdf5 | ||
|
8ceff5c1b6 | ||
|
0d1ab92b13 | ||
|
408f1a6d21 | ||
|
ed1815e173 | ||
|
3cc514a301 | ||
|
d4d9108947 | ||
|
1559e69de6 | ||
|
b11996e13d | ||
|
710b945b03 | ||
|
a6ed00ad33 | ||
|
cd434cf68c | ||
|
904ffcd212 | ||
|
0d06e096c5 | ||
|
a1c78c4ab9 | ||
|
cc3772680f | ||
|
a21cfc5297 | ||
|
587e30aafb | ||
|
03680eccc6 | ||
|
9b568aa8ec | ||
|
a82624023f | ||
|
93b83a5874 | ||
|
7c9a1bef86 | ||
|
4cb260c7dd | ||
|
5cc8ba9d8e | ||
|
7cbc75f8e9 | ||
|
0c46a82b10 | ||
|
309958deed | ||
|
f1b780af7d | ||
|
ffe0ce1f52 | ||
|
8799c26191 | ||
|
4b793bf2f9 | ||
|
734fa5d46d | ||
|
5e898858ae | ||
|
dcec0e6ad3 | ||
|
e9bea839b3 | ||
|
191bbf15bb | ||
|
c43d164c6a | ||
|
6895b66550 | ||
|
580ed6414c | ||
|
1bbb6a9fd7 | ||
|
6fe642e06d | ||
|
e041f3e2ea | ||
|
8b0edc8dae | ||
|
25f46c10a8 | ||
|
22ab18026b | ||
|
4023521f95 | ||
|
435983f26b | ||
|
f778e29b30 | ||
|
eacc74d24a | ||
|
587a1c796d | ||
|
4b014b7b61 | ||
|
673c193228 | ||
|
2c6e84bbc1 | ||
|
9cf6c0f44f | ||
|
d0094ec0d1 | ||
|
e17b25708b | ||
|
b7d1c9c438 | ||
|
6e411de99b | ||
|
124c0d06b5 | ||
|
bd38bf536c | ||
|
3a36a9dd1b | ||
|
c39aaeee8f | ||
|
d802288906 | ||
|
40161efbb4 | ||
|
dfe32b602c | ||
|
7208982758 | ||
|
bf574607fb | ||
|
129512b067 | ||
|
ed07409dbc | ||
|
e1fa0867b7 | ||
|
440d5f7ab1 | ||
|
94c1f42dbe | ||
|
201f38d44e | ||
|
afdde26a18 | ||
|
34e417e57b | ||
|
1441544426 | ||
|
b0d401421e | ||
|
9f13b8e667 | ||
|
b63d016818 | ||
|
a21861918b | ||
|
178f7b41ab | ||
|
7553e91dc6 | ||
|
b8b39eb0b8 | ||
|
531c579f2d | ||
|
4aa61c7eca | ||
|
20cbf8aa05 | ||
|
06ba6b867e | ||
|
6a289583bf | ||
|
9e6cd4b0af | ||
|
1f887cef07 | ||
|
29a3b41e15 | ||
|
172ab16823 | ||
|
0975a6180b | ||
|
6318176516 | ||
|
1876a25922 | ||
|
0f5f173488 | ||
|
694c9853c6 | ||
|
97248cccdd | ||
|
f181445920 | ||
|
c51548165a | ||
|
69754bd830 | ||
|
34d6e622b7 | ||
|
68515dc40a | ||
|
560dc4acbe | ||
|
5098b28ada | ||
|
bd12b03e0c | ||
|
6a797945d5 | ||
|
cc8f6ec843 | ||
|
b7d0481a83 | ||
|
9722f5b862 | ||
|
d79895dc88 | ||
|
e57316341b | ||
|
e57abbe5d9 | ||
|
6d76df7524 | ||
|
f0aadc6dfe | ||
|
a05067410a | ||
|
1161b9f4e8 | ||
|
e3bcf79f09 | ||
|
da8e86d743 | ||
|
94e84d6054 | ||
|
010f7325d1 | ||
|
72f3e2157b | ||
|
75188d3e35 | ||
|
cfc9299165 | ||
|
7aef25c316 | ||
|
de656fa26b | ||
|
b662b86922 | ||
|
83284be303 | ||
|
2760fa0d1d | ||
|
158603d405 | ||
|
709426d8ef | ||
|
e9b79ea28f | ||
|
2a6b88e6a7 | ||
|
b80d5e4bab | ||
|
873ffb6b7d | ||
|
c28d57eaca | ||
|
7fd32e67c2 | ||
|
b1a3495a14 | ||
|
89aaf40fdb | ||
|
ba52396bec | ||
|
0f9caf5461 | ||
|
4afb51830c | ||
|
e330986da5 | ||
|
fd369f707f | ||
|
d23366fc98 | ||
|
72c30d31c7 | ||
|
b7e4312770 | ||
|
a59b0190ab | ||
|
7261ca90ef | ||
|
078c08eabf | ||
|
a31553e130 | ||
|
0cc5c0823e | ||
|
501933e3f9 | ||
|
1129d32b7b | ||
|
cda2d46ed9 | ||
|
0f4c36219f | ||
|
5bdc968ffe | ||
|
3163f33887 | ||
|
7cc1aad114 | ||
|
a4ee9a2e84 | ||
|
99ca1fc230 | ||
|
8e168d3a9d | ||
|
588ae96d66 | ||
|
cbd4ba967e | ||
|
9cb793cdea | ||
|
d01d2a27f4 | ||
|
d63ac6191a | ||
|
2f3157b64d | ||
|
f6f9e9db1f | ||
|
30ab71c92d | ||
|
16910b462b | ||
|
4547c245fa | ||
|
88dbc368d1 | ||
|
2901e85487 | ||
|
7d75d5c52f | ||
|
c83917206f | ||
|
5675d7fa2d | ||
|
e340321604 | ||
|
c2dc8e2eb9 | ||
|
766da33ccf | ||
|
428be1e0f4 | ||
|
98a25b3824 | ||
|
58ecbdb121 | ||
|
3a57e1fdc2 | ||
|
e0cae3139f | ||
|
c9e541c26b | ||
|
5445115b3b | ||
|
9fd6f66686 | ||
|
26c880c7a0 | ||
|
a59753569f | ||
|
1fdc6d088c | ||
|
eecffca741 | ||
|
ec040e420e | ||
|
32bca9dde5 | ||
|
8e894c7ac9 | ||
|
d6d0b74ce0 | ||
|
edcf24fa09 | ||
|
c3422edeb7 | ||
|
41ba23d7ab | ||
|
a96745c292 | ||
|
77623264ae | ||
|
7c25c3319d | ||
|
09db6d5d8a | ||
|
7b1b77fbba | ||
|
dc8290b51b | ||
|
67f9d4eb9f | ||
|
f4c55ff2cf | ||
|
c252db4598 | ||
|
9c8539f627 | ||
|
edf225e888 | ||
|
f5dfe75634 | ||
|
36a3bd2113 | ||
|
0737c3254e | ||
|
8fd39fcf9d | ||
|
a7fab69838 | ||
|
cdf7db51f6 | ||
|
5f3169c1bb | ||
|
c57677bdd3 | ||
|
801da61c0f | ||
|
4cc983dbe6 | ||
|
dbe6c18d3a | ||
|
c45c78bdc2 | ||
|
744b73db3d | ||
|
58ce046394 | ||
|
7f17ab0063 | ||
|
546ebbd349 | ||
|
4ccaa52b87 | ||
|
89387c51da | ||
|
04a47babdd | ||
|
02e4f87a5a | ||
|
b080d78253 | ||
|
a1f48fae47 | ||
|
b6f0f90ac5 | ||
|
1bee0986c7 | ||
|
2716e4f72a | ||
|
6a8e70657b | ||
|
86a0059af5 | ||
|
9a1cc94f99 | ||
|
9c4806a840 | ||
|
79f6d6df8a | ||
|
b1e08bf813 | ||
|
fa14e4a02b | ||
|
8d44e1e827 | ||
|
3990b8e064 | ||
|
6f1ae83fbe | ||
|
d078968d08 | ||
|
f98400f753 | ||
|
a9261f889f | ||
|
b5f6e6da98 | ||
|
d2ff0783a5 | ||
|
b1b69c7d94 | ||
|
879d6723a3 | ||
|
e3e56301ce | ||
|
805b9d89ad | ||
|
fc5de3c8d5 | ||
|
bb182033b9 | ||
|
5157e60379 | ||
|
620361348f | ||
|
b302543502 | ||
|
d50df04d09 | ||
|
0149093e11 | ||
|
4b6a934637 | ||
|
1e7cbc4062 | ||
|
adc503d6ae | ||
|
091dc17337 | ||
|
6c97003571 | ||
|
962d89fd40 | ||
|
41212f4b4c | ||
|
6c49ef7e4a | ||
|
709a69aa82 | ||
|
3073fb8fec | ||
|
f3cb4f2ce0 | ||
|
be39ecc00a | ||
|
eebe009873 | ||
|
f18fe48008 | ||
|
7699d06647 | ||
|
72f6b356c4 | ||
|
7aa8896997 | ||
|
16d9c94822 | ||
|
f1296e391b | ||
|
ee64d2ef9c | ||
|
bbf1c9f4f7 | ||
|
420a861061 | ||
|
2e3f95ebd6 | ||
|
6ae139d987 | ||
|
f5d9aab2e5 | ||
|
bd71075023 | ||
|
e921d994d1 | ||
|
4ae8cecd90 | ||
|
aec7f820ca | ||
|
403d87f7f5 | ||
|
43713fbe92 | ||
|
5f831ebb9f | ||
|
23b570b609 | ||
|
28d6817c49 | ||
|
a8b57653f3 | ||
|
a508849397 | ||
|
22ca94c674 | ||
|
aefd57d2ba | ||
|
0ce03e7987 | ||
|
a448c9d2c6 | ||
|
aa95fa2234 | ||
|
091c73038f | ||
|
589c52f769 | ||
|
b414174ee6 | ||
|
242035cb5d | ||
|
08360a861f | ||
|
16911e904e | ||
|
e9d3fb21f0 | ||
|
0657e05cc0 | ||
|
ee5b67bf3a | ||
|
59c00eb459 | ||
|
536b24c939 | ||
|
d34d87ddb0 | ||
|
92781a521c | ||
|
8b7990b9a9 | ||
|
169be092bd | ||
|
7fa34891ca | ||
|
67526f6cb8 | ||
|
fb1dc1ade2 | ||
|
915dbf7bd9 | ||
|
530d01fc9d | ||
|
ec94a76e23 | ||
|
d3046194bc | ||
|
7eff6e4555 | ||
|
4b2b391900 | ||
|
4f26b02de8 | ||
|
5c6fdb4a6e | ||
|
ef61531199 | ||
|
f230a4a769 | ||
|
3c0e270279 | ||
|
4609092ec4 | ||
|
7d0338ef3c | ||
|
8a172747ee | ||
|
616882912c | ||
|
113f290c59 | ||
|
371892fc7d | ||
|
cb9aa1fc1b | ||
|
709c307374 | ||
|
751adc04c3 | ||
|
cc07b914c1 | ||
|
e7ecf5654e | ||
|
0d0bb3f178 | ||
|
bfe852f034 | ||
|
d71a3a2b10 | ||
|
76f21076f9 | ||
|
8020d93144 | ||
|
1011db6372 | ||
|
fcf59455db | ||
|
bd162a9bd6 | ||
|
af8eb08433 | ||
|
ee3403d44d | ||
|
d4ad835fd8 | ||
|
2eb12c9aed | ||
|
8fca650f46 | ||
|
b1e7b2dd77 | ||
|
dc8206d9fd | ||
|
f549b98925 | ||
|
eea1e65c07 | ||
|
f2c9213e3e | ||
|
547d8f25f3 | ||
|
fd8a41759c | ||
|
c2ee188ad5 | ||
|
5839a7b021 | ||
|
19829d7cf4 | ||
|
54846648fd | ||
|
d50095e974 | ||
|
746990938c | ||
|
d531cf3813 | ||
|
09a7619995 | ||
|
06339a2699 | ||
|
e5e96dfd54 | ||
|
4ed69994a2 | ||
|
276200225b | ||
|
f22ef577cf | ||
|
02fd4e8733 | ||
|
706368a462 | ||
|
6b14bc3dc0 | ||
|
6d432171e5 | ||
|
af0c6d882e | ||
|
f8a2eb79d4 | ||
|
8ab5855ccc | ||
|
3689eb17d2 | ||
|
7814282d3d | ||
|
7aa35d8130 | ||
|
f9627fc2ae | ||
|
e6faa3f6be | ||
|
d5b3d3f94b | ||
|
8e8fb6c54f | ||
|
2e5f70dea8 | ||
|
63989af21f | ||
|
8e58a8ed10 | ||
|
10f1aaa479 | ||
|
761580ad84 | ||
|
b64990490d | ||
|
9e4ad3fd84 | ||
|
9cef9e01e5 | ||
|
94393419c2 | ||
|
25b1dcb472 | ||
|
89d0606d06 | ||
|
c90f40cf30 |
16
.cargo/config.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Statically link the vcruntime
|
||||
# https://users.rust-lang.org/t/static-vcruntime-distribute-windows-msvc-binaries-without-needing-to-deploy-vcruntime-dll/57599
|
||||
[target.'cfg(all(windows, target_env = "msvc"))']
|
||||
rustflags = [
|
||||
"-C",
|
||||
"link-args=/DEFAULTLIB:ucrt.lib /DEFAULTLIB:libvcruntime.lib libcmt.lib",
|
||||
"-C",
|
||||
"link-args=/NODEFAULTLIB:libvcruntimed.lib /NODEFAULTLIB:vcruntime.lib /NODEFAULTLIB:vcruntimed.lib",
|
||||
"-C",
|
||||
"link-args=/NODEFAULTLIB:libcmtd.lib /NODEFAULTLIB:msvcrt.lib /NODEFAULTLIB:msvcrtd.lib",
|
||||
"-C",
|
||||
"link-args=/NODEFAULTLIB:libucrt.lib /NODEFAULTLIB:libucrtd.lib /NODEFAULTLIB:ucrtd.lib",
|
||||
]
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
15
.editorconfig
Normal file
|
@ -0,0 +1,15 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{json,jsonc,json5}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
8
.gitattributes
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
* text=auto
|
||||
|
||||
# Ensure all lua files use LF
|
||||
*.lua eol=lf
|
||||
*.luau eol=lf
|
||||
|
||||
# Ensure all txt files within tests use LF
|
||||
tests/**/*.txt eol=lf
|
97
.github/workflows/ci.yaml
vendored
|
@ -2,31 +2,100 @@ name: CI
|
|||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: CI
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
fmt:
|
||||
name: Check formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
components: rustfmt
|
||||
|
||||
- name: Rustfmt
|
||||
run: cargo fmt -- --check
|
||||
- name: Install Tooling
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Check Formatting
|
||||
run: just fmt-check
|
||||
|
||||
analyze:
|
||||
needs: ["fmt"]
|
||||
name: Analyze and lint Luau files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Tooling
|
||||
uses: CompeyDev/setup-rokit@v0.1.2
|
||||
|
||||
- name: Analyze
|
||||
run: just analyze
|
||||
|
||||
ci:
|
||||
needs: ["fmt"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Windows x86_64
|
||||
runner-os: windows-latest
|
||||
cargo-target: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Linux x86_64
|
||||
runner-os: ubuntu-latest
|
||||
cargo-target: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: macOS x86_64
|
||||
runner-os: macos-13
|
||||
cargo-target: x86_64-apple-darwin
|
||||
|
||||
- name: macOS aarch64
|
||||
runner-os: macos-14
|
||||
cargo-target: aarch64-apple-darwin
|
||||
|
||||
name: CI - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runner-os }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
targets: ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --locked --verbose
|
||||
run: |
|
||||
cargo build \
|
||||
--workspace \
|
||||
--locked --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
- name: Lint
|
||||
run: |
|
||||
cargo clippy \
|
||||
--workspace \
|
||||
--locked --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Test
|
||||
run: cargo test
|
||||
run: |
|
||||
cargo test \
|
||||
--lib --workspace \
|
||||
--locked --all-features \
|
||||
--target ${{ matrix.cargo-target }}
|
||||
|
|
196
.github/workflows/release.yaml
vendored
|
@ -6,143 +6,153 @@ on:
|
|||
permissions:
|
||||
contents: write
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
init:
|
||||
name: Init
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
manifest_name: ${{ steps.get_name.outputs.value }}
|
||||
manifest_version: ${{ steps.get_version.outputs.value }}
|
||||
version: ${{ steps.get_version.outputs.value }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get name from manifest
|
||||
uses: SebRollen/toml-action@0ad94c4a52c402aaa76e14e8a43551163b6cedf9
|
||||
id: get_name
|
||||
with:
|
||||
file: Cargo.toml
|
||||
field: package.name
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get version from manifest
|
||||
uses: SebRollen/toml-action@0ad94c4a52c402aaa76e14e8a43551163b6cedf9
|
||||
uses: SebRollen/toml-action@v1.2.0
|
||||
id: get_version
|
||||
with:
|
||||
file: Cargo.toml
|
||||
file: crates/lune/Cargo.toml
|
||||
field: package.version
|
||||
|
||||
create-release:
|
||||
needs: ["init"]
|
||||
name: Create release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
# dry-run:
|
||||
# name: Dry-run
|
||||
# needs: ["init"]
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
- name: Create release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ needs.init.outputs.manifest_version }}
|
||||
release_name: ${{ needs.init.outputs.manifest_version }}
|
||||
draft: true
|
||||
# - name: Install Rust
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
upload-assets:
|
||||
needs: ["init", "create-release"]
|
||||
name: Upload assets
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
# - name: Publish (dry-run)
|
||||
# uses: katyo/publish-crates@v2
|
||||
# with:
|
||||
# dry-run: true
|
||||
# check-repo: true
|
||||
# registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Upload Selene types to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: lune.yml
|
||||
asset_name: lune.yml
|
||||
asset_content_type: application/x-yaml
|
||||
|
||||
- name: Upload Luau types to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: luneTypes.d.luau
|
||||
asset_name: luneTypes.d.luau
|
||||
asset_content_type: application/x-luau
|
||||
|
||||
release:
|
||||
needs: ["init", "create-release"]
|
||||
build:
|
||||
needs: ["init"] # , "dry-run"]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Windows x86_64
|
||||
runner-os: windows-latest
|
||||
artifact-name: ${{ needs.init.outputs.manifest_name }}-${{ needs.init.outputs.manifest_version }}-windows-x86_64
|
||||
artifact-name: lune-${{ needs.init.outputs.version }}-windows-x86_64
|
||||
cargo-target: x86_64-pc-windows-msvc
|
||||
|
||||
- name: Linux x86_64
|
||||
runner-os: ubuntu-latest
|
||||
artifact-name: ${{ needs.init.outputs.manifest_name }}-${{ needs.init.outputs.manifest_version }}-linux-x86_64
|
||||
artifact-name: lune-${{ needs.init.outputs.version }}-linux-x86_64
|
||||
cargo-target: x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Linux aarch64
|
||||
runner-os: ubuntu-latest
|
||||
artifact-name: lune-${{ needs.init.outputs.version }}-linux-aarch64
|
||||
cargo-target: aarch64-unknown-linux-gnu
|
||||
|
||||
- name: macOS x86_64
|
||||
runner-os: macos-latest
|
||||
artifact-name: ${{ needs.init.outputs.manifest_name }}-${{ needs.init.outputs.manifest_version }}-macos-x86_64
|
||||
artifact-name: lune-${{ needs.init.outputs.version }}-macos-x86_64
|
||||
cargo-target: x86_64-apple-darwin
|
||||
|
||||
- name: macOS aarch64
|
||||
runner-os: macos-latest
|
||||
artifact-name: lune-${{ needs.init.outputs.version }}-macos-aarch64
|
||||
cargo-target: aarch64-apple-darwin
|
||||
|
||||
name: Build - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runner-os }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.cargo-target }}
|
||||
override: true
|
||||
profile: minimal
|
||||
targets: ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v2
|
||||
|
||||
- name: Install build tooling (aarch64-unknown-linux-gnu)
|
||||
if: matrix.cargo-target == 'aarch64-unknown-linux-gnu'
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y musl-tools clang llvm
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
|
||||
|
||||
- name: Build binary
|
||||
run: cargo build --locked --release --all-features --target ${{ matrix.cargo-target }}
|
||||
env:
|
||||
CARGO_TARGET_DIR: output
|
||||
OPENSSL_STATIC: 1
|
||||
run: just build --locked --release --target ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Create binary archive
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p staging
|
||||
if [ "${{ matrix.runner-os }}" = "windows-latest" ]; then
|
||||
cp "output/${{ matrix.cargo-target }}/release/${{ needs.init.outputs.manifest_name }}.exe" staging/
|
||||
cd staging
|
||||
7z a ../release.zip *
|
||||
else
|
||||
cp "output/${{ matrix.cargo-target }}/release/${{ needs.init.outputs.manifest_name }}" staging/
|
||||
cd staging
|
||||
zip ../release.zip *
|
||||
fi
|
||||
- name: Create release archive
|
||||
run: just zip-release ${{ matrix.cargo-target }}
|
||||
|
||||
- name: Upload binary artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: Upload release artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact-name }}
|
||||
path: release.zip
|
||||
|
||||
- name: Upload binary to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
release-github:
|
||||
name: Release (GitHub)
|
||||
runs-on: ubuntu-latest
|
||||
needs: ["init", "build"] # , "dry-run", "build"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Just
|
||||
uses: extractions/setup-just@v2
|
||||
|
||||
- name: Download releases
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ./releases
|
||||
|
||||
- name: Unpack releases
|
||||
run: just unpack-releases "./releases"
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: release.zip
|
||||
asset_name: ${{ matrix.artifact-name }}.zip
|
||||
asset_content_type: application/zip
|
||||
name: ${{ needs.init.outputs.version }}
|
||||
tag_name: v${{ needs.init.outputs.version }}
|
||||
fail_on_unmatched_files: true
|
||||
files: ./releases/*.zip
|
||||
draft: true
|
||||
|
||||
# release-crates:
|
||||
# name: Release (crates.io)
|
||||
# runs-on: ubuntu-latest
|
||||
# needs: ["init", "dry-run", "build"]
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v4
|
||||
|
||||
# - name: Install Rust
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
# - name: Publish crates
|
||||
# uses: katyo/publish-crates@v2
|
||||
# with:
|
||||
# dry-run: false
|
||||
# check-repo: true
|
||||
# registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
|
29
.gitignore
vendored
|
@ -1,4 +1,27 @@
|
|||
/target
|
||||
# Annoying macOS finder stuff
|
||||
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
/.DS_Store
|
||||
/**/.DS_Store
|
||||
|
||||
# Autogenerated dirs
|
||||
|
||||
/bin
|
||||
/out
|
||||
/target
|
||||
/staging
|
||||
|
||||
/**/bin
|
||||
/**/out
|
||||
/**/target
|
||||
/**/staging
|
||||
|
||||
# Autogenerated files
|
||||
|
||||
lune.yml
|
||||
luneDocs.json
|
||||
luneTypes.d.luau
|
||||
|
||||
# Files generated by runtime or build scripts
|
||||
scripts/brick_color.rs
|
||||
scripts/font_enum_map.rs
|
||||
scripts/physical_properties_enum_map.rs
|
||||
|
|
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "tests/roblox/rbx-test-files"]
|
||||
path = tests/roblox/rbx-test-files
|
||||
url = https://github.com/rojo-rbx/rbx-test-files
|
129
.justfile
Normal file
|
@ -0,0 +1,129 @@
|
|||
EXT := if os() == "windows" { ".exe" } else { "" }
|
||||
CWD := invocation_directory()
|
||||
BIN_NAME := "lune"
|
||||
|
||||
# Default hidden recipe for listing other recipes + cwd
|
||||
[no-cd]
|
||||
[no-exit-message]
|
||||
[private]
|
||||
default:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf "Current directory:\n {{CWD}}\n"
|
||||
just --list
|
||||
|
||||
# Builds the Lune CLI binary
|
||||
[no-exit-message]
|
||||
build *ARGS:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo build --bin {{BIN_NAME}} {{ARGS}}
|
||||
|
||||
# Run an individual file using the Lune CLI
|
||||
[no-exit-message]
|
||||
run FILE_PATH:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo run --bin {{BIN_NAME}} -- run "{{FILE_PATH}}"
|
||||
|
||||
# Run tests for the Lune library
|
||||
[no-exit-message]
|
||||
test *ARGS:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo test --lib -- {{ARGS}}
|
||||
|
||||
# Run tests for the Lune binary
|
||||
[no-exit-message]
|
||||
test-bin *ARGS:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cargo test --bin {{BIN_NAME}} -- {{ARGS}}
|
||||
|
||||
# Apply formatting for all Rust & Luau files
|
||||
[no-exit-message]
|
||||
fmt:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
stylua .lune scripts tests types \
|
||||
--glob "tests/**/*.luau" \
|
||||
--glob "!tests/roblox/rbx-test-files/**"
|
||||
cargo fmt
|
||||
|
||||
# Check formatting for all Rust & Luau files
|
||||
[no-exit-message]
|
||||
fmt-check:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
stylua .lune scripts tests types \
|
||||
--glob "tests/**/*.luau" \
|
||||
--glob "!tests/roblox/rbx-test-files/**"
|
||||
cargo fmt --check
|
||||
|
||||
# Analyze and lint Luau files using luau-lsp
|
||||
[no-exit-message]
|
||||
analyze:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
luau-lsp analyze \
|
||||
--settings=".vscode/settings.json" \
|
||||
--ignore="tests/roblox/rbx-test-files/**" \
|
||||
.lune scripts tests types
|
||||
|
||||
# Zips up the built binary into a single zip file
|
||||
[no-exit-message]
|
||||
zip-release TARGET_TRIPLE:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
rm -rf staging
|
||||
rm -rf release.zip
|
||||
mkdir -p staging
|
||||
cp "target/{{TARGET_TRIPLE}}/release/{{BIN_NAME}}{{EXT}}" staging/
|
||||
cd staging
|
||||
if [ "{{os_family()}}" = "windows" ]; then
|
||||
7z a ../release.zip *
|
||||
else
|
||||
chmod +x {{BIN_NAME}}
|
||||
zip ../release.zip *
|
||||
fi
|
||||
cd "{{CWD}}"
|
||||
rm -rf staging
|
||||
|
||||
# Used in GitHub workflow to move per-matrix release zips
|
||||
[no-exit-message]
|
||||
[private]
|
||||
unpack-releases RELEASES_DIR:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
#
|
||||
if [ ! -d "{{RELEASES_DIR}}" ]; then
|
||||
echo "Releases directory is missing"
|
||||
exit 1
|
||||
fi
|
||||
#
|
||||
cd "{{RELEASES_DIR}}"
|
||||
echo ""
|
||||
echo "Releases dir:"
|
||||
ls -lhrt
|
||||
echo ""
|
||||
echo "Searching for zipped releases..."
|
||||
#
|
||||
for DIR in * ; do
|
||||
if [ -d "$DIR" ]; then
|
||||
cd "$DIR"
|
||||
for FILE in * ; do
|
||||
if [ ! -d "$FILE" ]; then
|
||||
if [ "$FILE" = "release.zip" ]; then
|
||||
echo "Found zipped release '$DIR'"
|
||||
mv "$FILE" "../$DIR.zip"
|
||||
rm -rf "../$DIR/"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
#
|
||||
echo ""
|
||||
echo "Releases dir:"
|
||||
ls -lhrt
|
19
.luaurc
|
@ -1,7 +1,16 @@
|
|||
{
|
||||
"languageMode": "strict",
|
||||
"lint": { "*": true },
|
||||
"lintErrors": false,
|
||||
"typeErrors": true,
|
||||
"globals": []
|
||||
"languageMode": "strict",
|
||||
"lint": {
|
||||
"*": true
|
||||
},
|
||||
"lintErrors": false,
|
||||
"typeErrors": true,
|
||||
"globals": [
|
||||
"warn"
|
||||
],
|
||||
"aliases": {
|
||||
"lune": "./types/",
|
||||
"tests": "./tests",
|
||||
"require-tests": "./tests/require/tests"
|
||||
}
|
||||
}
|
||||
|
|
63
.lune/csv_printer.luau
Normal file
|
@ -0,0 +1,63 @@
|
|||
--> A utility script that prints out a CSV
|
||||
--> file in a prettified format to stdout
|
||||
|
||||
local LINE_SEPARATOR = "\n"
|
||||
local COMMA_SEPARATOR = ","
|
||||
|
||||
local fs = require("@lune/fs")
|
||||
local process = require("@lune/process")
|
||||
|
||||
local path = process.args[1] or ".lune/data/test.csv"
|
||||
|
||||
assert(path ~= nil and #path > 0, "No input file path was given")
|
||||
assert(not fs.isDir(path), "Input file path was a dir, not a file")
|
||||
assert(fs.isFile(path), "Input file path does not exist")
|
||||
|
||||
-- Read all the lines of the wanted file, and then split
|
||||
-- out the raw lines containing comma-separated values
|
||||
|
||||
local csvTable = {}
|
||||
for index, rawLine in string.split(fs.readFile(path), LINE_SEPARATOR) do
|
||||
if #rawLine > 0 then
|
||||
csvTable[index] = string.split(rawLine, COMMA_SEPARATOR)
|
||||
end
|
||||
end
|
||||
|
||||
-- Gather the maximum widths of strings
|
||||
-- for alignment & spacing in advance
|
||||
|
||||
local maxWidths = {}
|
||||
for _, row in csvTable do
|
||||
for index, value in row do
|
||||
maxWidths[index] = math.max(maxWidths[index] or 0, #value)
|
||||
end
|
||||
end
|
||||
|
||||
local totalWidth = 0
|
||||
local totalColumns = 0
|
||||
for _, width in maxWidths do
|
||||
totalWidth += width
|
||||
totalColumns += 1
|
||||
end
|
||||
|
||||
-- We have everything we need, print it out with
|
||||
-- the help of some unicode box drawing characters
|
||||
|
||||
local thiccLine = string.rep("━", totalWidth + totalColumns * 3 - 1)
|
||||
|
||||
print(string.format("┏%s┓", thiccLine))
|
||||
|
||||
for rowIndex, row in csvTable do
|
||||
local paddedValues = {}
|
||||
for valueIndex, value in row do
|
||||
local spacing = string.rep(" ", maxWidths[valueIndex] - #value)
|
||||
table.insert(paddedValues, value .. spacing)
|
||||
end
|
||||
print(string.format("┃ %s ┃", table.concat(paddedValues, " ┃ ")))
|
||||
-- The first line is the header, we should
|
||||
-- print out an extra separator below it
|
||||
if rowIndex == 1 then
|
||||
print(string.format("┣%s┫", thiccLine))
|
||||
end
|
||||
end
|
||||
print(string.format("┗%s┛", thiccLine))
|
4
.lune/data/test.csv
Normal file
|
@ -0,0 +1,4 @@
|
|||
Header1,Header2,Header3
|
||||
Hello,World,!
|
||||
1,2,3
|
||||
Foo,Bar,Baz
|
|
|
@ -1,63 +1,99 @@
|
|||
print("Hello, lune! 🌙")
|
||||
local fs = require("@lune/fs")
|
||||
local net = require("@lune/net")
|
||||
local process = require("@lune/process")
|
||||
local stdio = require("@lune/stdio")
|
||||
local task = require("@lune/task")
|
||||
|
||||
--[==[
|
||||
--[[
|
||||
EXAMPLE #1
|
||||
|
||||
Using a function from another module
|
||||
]==]
|
||||
|
||||
local module = require(".lune/module")
|
||||
module.sayHello()
|
||||
|
||||
--[==[
|
||||
EXAMPLE #2
|
||||
|
||||
Using arguments given to the program
|
||||
]==]
|
||||
]]
|
||||
|
||||
if #process.args > 0 then
|
||||
print("\nGot arguments while running hello_lune:")
|
||||
console.log(process.args)
|
||||
print("Got arguments:")
|
||||
print(process.args)
|
||||
if #process.args > 3 then
|
||||
error("Too many arguments!")
|
||||
end
|
||||
else
|
||||
print("Got no arguments ☹️")
|
||||
end
|
||||
|
||||
--[==[
|
||||
--[[
|
||||
EXAMPLE #2
|
||||
|
||||
Using the stdio library to prompt for terminal input
|
||||
]]
|
||||
|
||||
local text = stdio.prompt("text", "Please write some text")
|
||||
|
||||
print("You wrote '" .. text .. "'!")
|
||||
|
||||
local confirmed = stdio.prompt("confirm", "Please confirm that you wrote some text")
|
||||
if confirmed == false then
|
||||
error("You didn't confirm!")
|
||||
else
|
||||
print("Confirmed!")
|
||||
end
|
||||
|
||||
--[[
|
||||
EXAMPLE #3
|
||||
|
||||
Get & set environment variables
|
||||
|
||||
Checks if environment variables are empty or not,
|
||||
prints out ❌ if empty and ✅ if they have a value
|
||||
]==]
|
||||
]]
|
||||
|
||||
print("\nReading current environment 🔎")
|
||||
print("Reading current environment 🔎")
|
||||
|
||||
-- Environment variables can be read directly
|
||||
assert(process.env.PATH ~= nil, "Missing PATH")
|
||||
assert(process.env.PWD ~= nil, "Missing PWD")
|
||||
|
||||
-- And they can also be accessed using generalized iteration (not pairs!)
|
||||
-- And they can also be accessed using Luau's generalized iteration (but not pairs())
|
||||
for key, value in process.env do
|
||||
local box = if value and value ~= "" then "✅" else "❌"
|
||||
print(string.format("[%s] %s", box, key))
|
||||
end
|
||||
|
||||
--[==[
|
||||
--[[
|
||||
EXAMPLE #4
|
||||
|
||||
Spawning concurrent tasks
|
||||
|
||||
These tasks will run at the same time as other Lua code which lets you do primitive multitasking
|
||||
]]
|
||||
|
||||
task.spawn(function()
|
||||
print("Spawned a task that will run instantly but not block")
|
||||
task.wait(5)
|
||||
end)
|
||||
|
||||
print("Spawning a delayed task that will run in 5 seconds")
|
||||
task.delay(5, function()
|
||||
print("...")
|
||||
task.wait(1)
|
||||
print("Hello again!")
|
||||
task.wait(1)
|
||||
print("Goodbye again! 🌙")
|
||||
end)
|
||||
|
||||
--[[
|
||||
EXAMPLE #5
|
||||
|
||||
Read files in the current directory
|
||||
|
||||
This prints out directory & file names with some fancy icons
|
||||
]==]
|
||||
]]
|
||||
|
||||
print("\nReading current dir 🗂️")
|
||||
print("Reading current dir 🗂️")
|
||||
local entries = fs.readDir(".")
|
||||
|
||||
-- NOTE: We have to do this outside of the sort function
|
||||
-- to avoid yielding across the metamethod boundary, any
|
||||
-- calls to fs functions may yield for any reason
|
||||
-- to avoid yielding across the metamethod boundary, all
|
||||
-- of the filesystem APIs are asynchronous and yielding
|
||||
local entryIsDir = {}
|
||||
for _, entry in entries do
|
||||
entryIsDir[entry] = fs.isDir(entry)
|
||||
|
@ -84,63 +120,58 @@ for _, entry in entries do
|
|||
end
|
||||
end
|
||||
|
||||
-- NOTE: We skip the ping example in GitHub Actions
|
||||
-- since the ping command does not work in azure
|
||||
if not process.env.GITHUB_ACTIONS then
|
||||
--[==[
|
||||
EXAMPLE #5
|
||||
--[[
|
||||
EXAMPLE #6
|
||||
|
||||
Call out to another program / executable
|
||||
Call out to another program / executable
|
||||
|
||||
Here we send some pings to google to demonstrate that programs
|
||||
that yield or perform any network requests work correctly
|
||||
]==]
|
||||
You can also get creative and combine this with example #6 to spawn several programs at the same time!
|
||||
]]
|
||||
|
||||
print("\nSending 4 pings to google 🌏")
|
||||
local result = process.spawn("ping", {
|
||||
"google.com",
|
||||
"-c 4",
|
||||
})
|
||||
print("Sending 4 pings to google 🌏")
|
||||
local result = process.exec("ping", {
|
||||
"google.com",
|
||||
"-c 4",
|
||||
})
|
||||
|
||||
--[==[
|
||||
EXAMPLE #6
|
||||
|
||||
Using the result of a spawned process, exiting the process
|
||||
|
||||
We use the result from the above ping command and parse
|
||||
it to show the results it gave us in a nicer format, here we
|
||||
also exit with an error (exit code 1) if spawning the process failed
|
||||
]==]
|
||||
|
||||
if result.ok then
|
||||
assert(#result.stdout > 0, "Result output was empty")
|
||||
local min, avg, max, stddev = string.match(
|
||||
result.stdout,
|
||||
"min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms"
|
||||
)
|
||||
print(string.format("Minimum ping time: %.3fms", assert(tonumber(min))))
|
||||
print(string.format("Maximum ping time: %.3fms", assert(tonumber(max))))
|
||||
print(string.format("Average ping time: %.3fms", assert(tonumber(avg))))
|
||||
print(string.format("Standard deviation: %.3fms", assert(tonumber(stddev))))
|
||||
else
|
||||
print("\nFailed to send ping to google!")
|
||||
print(result.stderr)
|
||||
process.exit(result.code)
|
||||
end
|
||||
end
|
||||
|
||||
--[==[
|
||||
--[[
|
||||
EXAMPLE #7
|
||||
|
||||
Using the built-in networking library
|
||||
]==]
|
||||
print("\nSending PATCH request to web API 📤")
|
||||
Using the result of a spawned process, exiting the process
|
||||
|
||||
This looks scary with lots of weird symbols, but, it's just some Lua-style pattern matching
|
||||
to parse the lines of "min/avg/max/stddev = W/X/Y/Z ms" that the ping program outputs to us
|
||||
]]
|
||||
|
||||
if result.ok then
|
||||
assert(#result.stdout > 0, "Result output was empty")
|
||||
local min, avg, max, stddev = string.match(
|
||||
result.stdout,
|
||||
"min/avg/max/stddev = ([%d%.]+)/([%d%.]+)/([%d%.]+)/([%d%.]+) ms"
|
||||
)
|
||||
print(string.format("Minimum ping time: %.3fms", assert(tonumber(min))))
|
||||
print(string.format("Maximum ping time: %.3fms", assert(tonumber(max))))
|
||||
print(string.format("Average ping time: %.3fms", assert(tonumber(avg))))
|
||||
print(string.format("Standard deviation: %.3fms", assert(tonumber(stddev))))
|
||||
else
|
||||
print("Failed to send ping to google!")
|
||||
print(result.stderr)
|
||||
process.exit(result.code)
|
||||
end
|
||||
|
||||
--[[
|
||||
EXAMPLE #8
|
||||
|
||||
Using the built-in networking library, encoding & decoding json
|
||||
]]
|
||||
|
||||
print("Sending PATCH request to web API 📤")
|
||||
local apiResult = net.request({
|
||||
url = "https://jsonplaceholder.typicode.com/posts/1",
|
||||
method = "PATCH",
|
||||
headers = {
|
||||
["Content-Type"] = "application/json",
|
||||
},
|
||||
} :: { [string]: string },
|
||||
body = net.jsonEncode({
|
||||
title = "foo",
|
||||
body = "bar",
|
||||
|
@ -148,7 +179,7 @@ local apiResult = net.request({
|
|||
})
|
||||
|
||||
if not apiResult.ok then
|
||||
print("\nFailed to send network request!")
|
||||
print("Failed to send network request!")
|
||||
print(string.format("%d (%s)", apiResult.statusCode, apiResult.statusMessage))
|
||||
print(apiResult.body)
|
||||
process.exit(1)
|
||||
|
@ -166,20 +197,18 @@ assert(apiResponse.title == "foo", "Invalid json response")
|
|||
assert(apiResponse.body == "bar", "Invalid json response")
|
||||
print("Got valid JSON response with changes applied")
|
||||
|
||||
--[==[
|
||||
EXAMPLE #8
|
||||
--[[
|
||||
EXAMPLE #9
|
||||
|
||||
Using the console library to print pretty
|
||||
]==]
|
||||
Using the stdio library to print pretty
|
||||
]]
|
||||
|
||||
print("\nPrinting with pretty colors and auto-formatting 🎨")
|
||||
print("Printing with pretty colors and auto-formatting 🎨")
|
||||
|
||||
console.setColor("blue")
|
||||
print(string.rep("—", 22))
|
||||
console.resetColor()
|
||||
print(stdio.color("blue") .. string.rep("—", 22) .. stdio.color("reset"))
|
||||
|
||||
console.info("API response:", apiResponse)
|
||||
console.warn({
|
||||
print("API response:", apiResponse)
|
||||
warn({
|
||||
Oh = {
|
||||
No = {
|
||||
TooMuch = {
|
||||
|
@ -191,14 +220,12 @@ console.warn({
|
|||
},
|
||||
})
|
||||
|
||||
console.setColor("blue")
|
||||
print(string.rep("—", 22))
|
||||
console.resetColor()
|
||||
print(stdio.color("blue") .. string.rep("—", 22) .. stdio.color("reset"))
|
||||
|
||||
--[==[
|
||||
EXAMPLE #9
|
||||
--[[
|
||||
EXAMPLE #10
|
||||
|
||||
Saying goodbye 😔
|
||||
]==]
|
||||
]]
|
||||
|
||||
print("\nGoodbye, lune! 🌙")
|
||||
print("Goodbye, lune! 🌙")
|
||||
|
|
53
.lune/http_server.luau
Normal file
|
@ -0,0 +1,53 @@
|
|||
--> A basic http server that echoes the given request
|
||||
--> body at /ping and otherwise responds 404 "Not Found"
|
||||
|
||||
local net = require("@lune/net")
|
||||
local process = require("@lune/process")
|
||||
local task = require("@lune/task")
|
||||
|
||||
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
|
||||
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
|
||||
else 8080
|
||||
|
||||
-- Create our responder functions
|
||||
|
||||
local function pong(request: net.ServeRequest): string
|
||||
return `Pong!\n{request.path}\n{request.body}`
|
||||
end
|
||||
|
||||
local function teapot(_request: net.ServeRequest): net.ServeResponse
|
||||
return {
|
||||
status = 418,
|
||||
body = "🫖",
|
||||
}
|
||||
end
|
||||
|
||||
local function notFound(_request: net.ServeRequest): net.ServeResponse
|
||||
return {
|
||||
status = 404,
|
||||
body = "Not Found",
|
||||
}
|
||||
end
|
||||
|
||||
-- Run the server on port 8080
|
||||
|
||||
local handle = net.serve(PORT, function(request)
|
||||
if string.sub(request.path, 1, 5) == "/ping" then
|
||||
return pong(request)
|
||||
elseif string.sub(request.path, 1, 7) == "/teapot" then
|
||||
return teapot(request)
|
||||
else
|
||||
return notFound(request)
|
||||
end
|
||||
end)
|
||||
|
||||
print(`Listening on port {PORT} 🚀`)
|
||||
|
||||
-- Exit our example after a small delay, if you copy this
|
||||
-- example just remove this part to keep the server running
|
||||
|
||||
task.delay(2, function()
|
||||
print("Shutting down...")
|
||||
task.wait(1)
|
||||
handle.stop()
|
||||
end)
|
|
@ -1,7 +0,0 @@
|
|||
local module = {}
|
||||
|
||||
function module.sayHello()
|
||||
print("\nHello from a module! 🧩")
|
||||
end
|
||||
|
||||
return module
|
44
.lune/websocket_client.luau
Normal file
|
@ -0,0 +1,44 @@
|
|||
--> A basic web socket client that communicates with an echo server
|
||||
|
||||
local net = require("@lune/net")
|
||||
local process = require("@lune/process")
|
||||
local task = require("@lune/task")
|
||||
|
||||
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
|
||||
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
|
||||
else 8080
|
||||
|
||||
local URL = `ws://127.0.0.1:{PORT}`
|
||||
|
||||
-- Connect to our web socket server
|
||||
|
||||
local socket = net.socket(URL)
|
||||
|
||||
print("Connected to echo web socket server at '" .. URL .. "'")
|
||||
print("Sending a message every second for 5 seconds...")
|
||||
|
||||
-- Force exit after 10 seconds in case the server is not responding well
|
||||
|
||||
local forceExit = task.delay(10, function()
|
||||
warn("Example did not complete in time, exiting...")
|
||||
process.exit(1)
|
||||
end)
|
||||
|
||||
-- Send one message per second and time it
|
||||
|
||||
for _ = 1, 5 do
|
||||
local start = os.clock()
|
||||
socket:send(tostring(1))
|
||||
local response = socket:next()
|
||||
local elapsed = os.clock() - start
|
||||
print(`Got response '{response}' in {elapsed * 1_000} milliseconds`)
|
||||
task.wait(1 - elapsed)
|
||||
end
|
||||
|
||||
-- Everything went well, and we are done with the socket, so we can close it
|
||||
|
||||
print("Closing web socket...")
|
||||
socket:close()
|
||||
|
||||
task.cancel(forceExit)
|
||||
print("Done! 🌙")
|
37
.lune/websocket_server.luau
Normal file
|
@ -0,0 +1,37 @@
|
|||
--> A basic web socket server that echoes given messages
|
||||
|
||||
local net = require("@lune/net")
|
||||
local process = require("@lune/process")
|
||||
local task = require("@lune/task")
|
||||
|
||||
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
|
||||
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
|
||||
else 8080
|
||||
|
||||
-- Run the server on port 8080, if we get a normal http request on
|
||||
-- the port this will respond with 426 Upgrade Required by default
|
||||
|
||||
local handle = net.serve(PORT, {
|
||||
handleWebSocket = function(socket)
|
||||
print("Got new web socket connection!")
|
||||
repeat
|
||||
local message = socket:next()
|
||||
if message ~= nil then
|
||||
socket:send("Echo - " .. message)
|
||||
end
|
||||
until message == nil
|
||||
print("Web socket disconnected.")
|
||||
end,
|
||||
})
|
||||
|
||||
print(`Listening on port {PORT} 🚀`)
|
||||
|
||||
-- Exit our example after a small delay, if you copy this
|
||||
-- example just remove this part to keep the server running
|
||||
|
||||
task.delay(10, function()
|
||||
print("Shutting down...")
|
||||
task.wait(1)
|
||||
handle.stop()
|
||||
task.wait(1)
|
||||
end)
|
8
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"rust-lang.rust-analyzer",
|
||||
"esbenp.prettier-vscode",
|
||||
"JohnnyMorganz.stylua",
|
||||
"DavidAnson.vscode-markdownlint"
|
||||
]
|
||||
}
|
41
.vscode/settings.json
vendored
|
@ -1,18 +1,27 @@
|
|||
{
|
||||
"luau-lsp.types.roblox": false,
|
||||
"luau-lsp.types.definitionFiles": ["luneTypes.d.luau"],
|
||||
"luau-lsp.sourcemap.enabled": false,
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"editor.formatOnSave": true,
|
||||
"stylua.searchParentDirectories": true,
|
||||
"prettier.tabWidth": 2,
|
||||
"[luau][lua]": {
|
||||
"editor.defaultFormatter": "JohnnyMorganz.stylua"
|
||||
},
|
||||
"[json][jsonc][markdown][yaml]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
}
|
||||
"luau-lsp.sourcemap.enabled": false,
|
||||
"luau-lsp.types.roblox": false,
|
||||
"luau-lsp.require.mode": "relativeToFile",
|
||||
"luau-lsp.require.directoryAliases": {
|
||||
"@lune/": "./types/",
|
||||
"@tests/": "./tests/",
|
||||
"@require-tests/": "./tests/require/tests/"
|
||||
},
|
||||
"luau-lsp.ignoreGlobs": [
|
||||
"tests/roblox/rbx-test-files/**/*.lua",
|
||||
"tests/roblox/rbx-test-files/**/*.luau"
|
||||
],
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"editor.formatOnSave": true,
|
||||
"stylua.searchParentDirectories": true,
|
||||
"prettier.tabWidth": 2,
|
||||
"[luau][lua]": {
|
||||
"editor.defaultFormatter": "JohnnyMorganz.stylua"
|
||||
},
|
||||
"[json][jsonc][markdown][yaml]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[rust]": {
|
||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||
}
|
||||
}
|
||||
|
|
1314
CHANGELOG.md
71
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,71 @@
|
|||
<!-- markdownlint-disable MD001 -->
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
|
||||
# Contributing
|
||||
|
||||
---
|
||||
|
||||
### Reporting a Bug
|
||||
|
||||
- Make sure the bug has not already been reported by searching on GitHub under [Issues](https://github.com/lune-org/lune/issues).
|
||||
- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/lune-org/lune/issues/new). Be sure to include a **title and description**, as much relevant information as possible, and if applicable, a **code sample** or a **test case** demonstrating the expected behavior.
|
||||
|
||||
---
|
||||
|
||||
### Contributing - Bug Fixes
|
||||
|
||||
1. Make sure an [issue](https://github.com/lune-org/lune/issues) has been created for the bug first, so that it can be tracked and searched for in the repository history. This is not mandatory for small fixes.
|
||||
2. Open a new GitHub pull request for it. A pull request for a bug fix must include:
|
||||
- A clear and concise description of the bug it is fixing.
|
||||
- A new test file ensuring there are no regressions after the bug has been fixed.
|
||||
- A link to the relevant issue, or a `Fixes #issue` line, if an issue exists.
|
||||
|
||||
### Contributing - Features
|
||||
|
||||
1. Make sure an [issue](https://github.com/lune-org/lune/issues) has been created for the feature first, so that it can be tracked and searched for in the repository history. If you are making changes to an existing feature, and no issue exists, one should be created for the proposed changes.
|
||||
2. Any API design or considerations should first be brought up and discussed in the relevant issue, to prevent long review times on pull requests and unnecessary work for maintainers.
|
||||
3. Familiarize yourself with the codebase and the tools you will be using. Some important parts include:
|
||||
- The [mlua](https://crates.io/crates/mlua) library, which we use to interface with Luau.
|
||||
- Any [built-in libraries](https://github.com/lune-org/lune/tree/main/src/lune/builtins) that are relevant for your new feature. If you are making a new built-in library, refer to existing ones for structure and implementation details.
|
||||
- Our toolchain, notably [StyLua](https://github.com/JohnnyMorganz/StyLua), [rustfmt](https://github.com/rust-lang/rustfmt), and [clippy](https://github.com/rust-lang/rust-clippy). If you do not use these tools there is a decent chance CI will fail on your pull request, blocking it from getting approved.
|
||||
4. Write some code!
|
||||
5. Open a new GitHub pull request. A pull request for a feature must include:
|
||||
- A clear and concise description of the new feature or changes to the feature.
|
||||
- Test files for any added or changed functionality.
|
||||
- A link to the relevant issue, or a `Closes #issue` line.
|
||||
|
||||
### Contributing - Formatting & Cosmetic Changes
|
||||
|
||||
Changes that are purely cosmetic, and do not add to the stability, functionality, or testability of Lune, will generally not be accepted unless there has been previous discussion about the changes being made.
|
||||
|
||||
### Contributing - Documentation
|
||||
|
||||
#### Documentation Site
|
||||
|
||||
Check out the [docs](https://github.com/lune-org/docs) repository and its contribution guidelines.
|
||||
|
||||
#### Type Definitions
|
||||
|
||||
If type definitions for built-in libraries need improvements:
|
||||
|
||||
1. Check out the [types](https://github.com/lune-org/lune/tree/main/types) directory at the root of the repository.
|
||||
2. Make the desired changes, and verify that they have the desired outcome.
|
||||
3. Open a new GitHub pull request for your changes.
|
||||
|
||||
---
|
||||
|
||||
### Publishing a Release
|
||||
|
||||
The Lune release process is semi-automated, and takes care of most things for you. Here's how to create a new release:
|
||||
|
||||
1. Make sure the changelog is up to date and contains all of the changes since the last release.
|
||||
2. Add the release date in the changelog + set a new version number in `Cargo.toml`.
|
||||
3. Commit and push changes from step 2 to GitHub. This will automatically publish the Lune library to [crates.io](https://crates.io) when the version number changes.
|
||||
4. Trigger the [release](https://github.com/lune-org/lune/actions/workflows/release.yaml) workflow on GitHub manually, and wait for it to finish. Find the new pending release in the [Releases](https://github.com/lune-org/lune/releases) section.
|
||||
5. Add in changes from the changelog for the new pending release into the description, hit "accept" on creating a new version tag, and publish 🚀
|
||||
|
||||
---
|
||||
|
||||
If you have any questions, check out the `#lune` channel in the [Roblox OSS discord](https://discord.gg/H9WqmFAB5Y), where most of our realtime discussion takes place!
|
||||
|
||||
Thank you for contributing to Lune! 🌙
|
3664
Cargo.lock
generated
87
Cargo.toml
|
@ -1,29 +1,64 @@
|
|||
[package]
|
||||
name = "lune"
|
||||
version = "0.0.4"
|
||||
edition = "2021"
|
||||
license = "MPL 2.0"
|
||||
repository = "https://github.com/filiptibell/lune"
|
||||
description = "A Luau script runner"
|
||||
keywords = ["cli", "lua", "luau", "scripts"]
|
||||
categories = ["command-line-interface"]
|
||||
|
||||
[[bin]]
|
||||
name = "lune"
|
||||
path = "src/main.rs"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
default-members = ["crates/lune"]
|
||||
members = [
|
||||
"crates/lune",
|
||||
"crates/lune-roblox",
|
||||
"crates/lune-std",
|
||||
"crates/lune-std-datetime",
|
||||
"crates/lune-std-fs",
|
||||
"crates/lune-std-luau",
|
||||
"crates/lune-std-net",
|
||||
"crates/lune-std-process",
|
||||
"crates/lune-std-regex",
|
||||
"crates/lune-std-roblox",
|
||||
"crates/lune-std-serde",
|
||||
"crates/lune-std-stdio",
|
||||
"crates/lune-std-task",
|
||||
"crates/lune-utils",
|
||||
"crates/mlua-luau-scheduler",
|
||||
]
|
||||
|
||||
# Profile for building the release binary, with the following options set:
|
||||
#
|
||||
# 1. Optimize for size
|
||||
# 2. Automatically strip symbols from the binary
|
||||
# 3. Enable link-time optimization
|
||||
#
|
||||
# Note that we could abort instead of panicking to cut down on size
|
||||
# even more, but because we use the filesystem & some other APIs we
|
||||
# need the panic unwinding to properly handle usage of said APIs
|
||||
#
|
||||
[profile.release]
|
||||
strip = true # Automatically strip symbols from the binary.
|
||||
opt-level = "z" # Optimize for size.
|
||||
lto = true # Enable link-time optimization
|
||||
panic = "abort" # Remove extra panic info
|
||||
opt-level = "z"
|
||||
strip = true
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.68" }
|
||||
clap = { version = "4.1.1", features = ["derive"] }
|
||||
mlua = { version = "0.8.7", features = ["luau", "async", "serialize"] }
|
||||
os_str_bytes = "6.4.1"
|
||||
reqwest = { version = "0.11.13", features = ["gzip", "deflate"] }
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = { version = "1.0.91" }
|
||||
tokio = { version = "1.24.2", features = ["full"] }
|
||||
# Lints for all crates in the workspace
|
||||
#
|
||||
# 1. Error on all lints by default, then make cargo + clippy pedantic lints just warn
|
||||
# 2. Selectively allow some lints that are _too_ pedantic, such as:
|
||||
# - Casts between number types
|
||||
# - Module naming conventions
|
||||
# - Imports and multiple dependency versions
|
||||
[workspace.lints.clippy]
|
||||
all = { level = "deny", priority = -3 }
|
||||
cargo = { level = "warn", priority = -2 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
|
||||
cast_lossless = { level = "allow", priority = 1 }
|
||||
cast_possible_truncation = { level = "allow", priority = 1 }
|
||||
cast_possible_wrap = { level = "allow", priority = 1 }
|
||||
cast_precision_loss = { level = "allow", priority = 1 }
|
||||
cast_sign_loss = { level = "allow", priority = 1 }
|
||||
|
||||
similar_names = { level = "allow", priority = 1 }
|
||||
unnecessary_wraps = { level = "allow", priority = 1 }
|
||||
unnested_or_patterns = { level = "allow", priority = 1 }
|
||||
unreadable_literal = { level = "allow", priority = 1 }
|
||||
|
||||
multiple_crate_versions = { level = "allow", priority = 1 }
|
||||
module_inception = { level = "allow", priority = 1 }
|
||||
module_name_repetitions = { level = "allow", priority = 1 }
|
||||
needless_pass_by_value = { level = "allow", priority = 1 }
|
||||
wildcard_imports = { level = "allow", priority = 1 }
|
||||
|
|
|
@ -370,4 +370,4 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
|
|||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
|
|
210
README.md
|
@ -1,190 +1,48 @@
|
|||
<!-- markdownlint-disable MD033 -->
|
||||
<!-- markdownlint-disable MD041 -->
|
||||
|
||||
# Lune 🌙
|
||||
<img align="right" width="250" src="assets/logo/tilt_svg.svg" alt="Lune logo" />
|
||||
|
||||
[](https://github.com/filiptibell/lune/actions/workflows/ci.yaml)
|
||||
[](https://github.com/filiptibell/lune/actions/workflows/release.yaml)
|
||||
<h1 align="center">Lune</h1>
|
||||
|
||||
A [Luau](https://luau-lang.org) script runner
|
||||
<div align="center">
|
||||
<div>
|
||||
<a href="https://crates.io/crates/lune">
|
||||
<img src="https://img.shields.io/crates/v/lune.svg?label=Version" alt="Current Lune library version" />
|
||||
</a>
|
||||
<a href="https://github.com/lune-org/lune/actions">
|
||||
<img src="https://shields.io/endpoint?url=https://badges.readysetplay.io/workflow/lune-org/lune/ci.yaml" alt="CI status" />
|
||||
</a>
|
||||
<a href="https://github.com/lune-org/lune/actions">
|
||||
<img src="https://shields.io/endpoint?url=https://badges.readysetplay.io/workflow/lune-org/lune/release.yaml" alt="Release status" />
|
||||
</a>
|
||||
<a href="https://github.com/lune-org/lune/blob/main/LICENSE.txt">
|
||||
<img src="https://img.shields.io/github/license/lune-org/lune.svg?label=License&color=informational" alt="Lune license" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
---
|
||||
<br/>
|
||||
|
||||
🚀 Use the ergonomics and readability of Luau instead of shell scripts 🚀
|
||||
A standalone [Luau](https://luau-lang.org) runtime.
|
||||
|
||||
[Full example & walkthrough](.lune/hello_lune.luau)
|
||||
Write and run programs, similar to runtimes for other languages such as [Node](https://nodejs.org), [Deno](https://deno.land), [Bun](https://bun.sh), or [Luvit](https://luvit.io) for vanilla Lua.
|
||||
|
||||
## ⚙️ Installation
|
||||
Lune provides fully asynchronous APIs wherever possible, and is built in Rust 🦀 for speed, safety and correctness.
|
||||
|
||||
The preferred way of installing Lune is using [Aftman](https://github.com/lpghatguy/aftman).
|
||||
## Features
|
||||
|
||||
This will add `lune` to an `aftman.toml` file in the current directory, or create one if it does not exist:
|
||||
- 🌙 Strictly minimal but powerful interface that is easy to read and remember, just like Luau itself
|
||||
- 🧰 Fully featured APIs for the filesystem, networking, stdio, all included in the small (~5mb zipped) executable
|
||||
- 📚 World-class documentation, on the web _or_ directly in your editor, no network connection necessary
|
||||
- 🏡 Familiar runtime environment for Roblox developers, with an included 1-to-1 task scheduler port
|
||||
- ✏️ Optional built-in library for manipulating Roblox place & model files, and their instances
|
||||
|
||||
```sh
|
||||
aftman add filiptibell/lune
|
||||
```
|
||||
## Non-goals
|
||||
|
||||
You can also download pre-built binaries for most systems directly from the GitHub Releases page.
|
||||
- Making programs short and terse - proper autocomplete / intellisense make using Lune just as quick, and readability is important
|
||||
- Running full Roblox games outside of Roblox - there is some compatibility, but Lune is meant for different purposes
|
||||
|
||||
## ✏️ Writing Lune Scripts
|
||||
## Where do I start?
|
||||
|
||||
Check out the examples of how to write a script in the [.lune](.lune) folder !
|
||||
|
||||
<details>
|
||||
<summary><b>🔎 Full list of APIs</b></summary>
|
||||
|
||||
<details>
|
||||
<summary><b>console</b> - Logging & formatting</summary>
|
||||
|
||||
```lua
|
||||
type console = {
|
||||
resetColor: () -> (),
|
||||
setColor: (color: "black" | "red" | "green" | "yellow" | "blue" | "purple" | "cyan" | "white") -> (),
|
||||
resetStyle: () -> (),
|
||||
setStyle: (color: "bold" | "dim") -> (),
|
||||
format: (...any) -> (string),
|
||||
log: (...any) -> (),
|
||||
info: (...any) -> (),
|
||||
warn: (...any) -> (),
|
||||
error: (...any) -> (),
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>fs</b> - Filesystem</summary>
|
||||
|
||||
```lua
|
||||
type fs = {
|
||||
readFile: (path: string) -> string,
|
||||
readDir: (path: string) -> { string },
|
||||
writeFile: (path: string, contents: string) -> (),
|
||||
writeDir: (path: string) -> (),
|
||||
removeFile: (path: string) -> (),
|
||||
removeDir: (path: string) -> (),
|
||||
isFile: (path: string) -> boolean,
|
||||
isDir: (path: string) -> boolean,
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>net</b> - Networking</summary>
|
||||
|
||||
```lua
|
||||
type net = {
|
||||
request: (config: string | {
|
||||
url: string,
|
||||
method: ("GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH")?,
|
||||
headers: { [string]: string }?,
|
||||
body: string?,
|
||||
}) -> {
|
||||
ok: boolean,
|
||||
statusCode: number,
|
||||
statusMessage: string,
|
||||
headers: { [string]: string },
|
||||
body: string,
|
||||
},
|
||||
jsonEncode: (value: any, pretty: boolean?) -> string,
|
||||
jsonDecode: (encoded: string) -> any,
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>process</b> - Current process & child processes</summary>
|
||||
|
||||
```lua
|
||||
type process = {
|
||||
args: { string },
|
||||
env: { [string]: string? },
|
||||
exit: (code: number?) -> (),
|
||||
spawn: (program: string, params: { string }?) -> {
|
||||
ok: boolean,
|
||||
code: number,
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🔀 Example translation from Bash</b></summary>
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
VALID=true
|
||||
COUNT=1
|
||||
while [ $VALID ]
|
||||
do
|
||||
echo $COUNT
|
||||
if [ $COUNT -eq 5 ];
|
||||
then
|
||||
break
|
||||
fi
|
||||
((COUNT++))
|
||||
done
|
||||
```
|
||||
|
||||
**_With Lune & Luau:_**
|
||||
|
||||
```lua
|
||||
local valid = true
|
||||
local count = 1
|
||||
while valid do
|
||||
print(count)
|
||||
if count == 5 then
|
||||
break
|
||||
end
|
||||
count += 1
|
||||
end
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>🧑💻 Configuring VSCode for Lune</b></summary>
|
||||
|
||||
Lune puts developer experience first, and as such provides type definitions and configurations for several tools out of the box.
|
||||
|
||||
<details>
|
||||
<summary>Luau LSP</summary>
|
||||
|
||||
1. Use `lune --download-luau-types` to download Luau types (`luneTypes.d.luau`) to the current directory
|
||||
2. Set your definition files setting to include `luneTypes.d.luau`, an example can be found in the [.vscode](.vscode) folder in this repository
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Selene</summary>
|
||||
|
||||
1. Use `lune --download-selene-types` to download Selene types (`lune.yml`) to the current directory
|
||||
2. Use either `std = "roblox-lune"` or `std = "luau+lune"` in your `selene.toml` configuration file
|
||||
|
||||
</details>
|
||||
|
||||
**_NOTE:_** _It is highly recommended to add any type definition files to your `.gitignore` and to only download them using these commands, since this guarantees that you have type definitions compatible with your installed version of Lune._
|
||||
|
||||
</details>
|
||||
|
||||
## 🏃 Running Lune Scripts
|
||||
|
||||
When you've written a script with either a `.lua` or `.luau` extension, you can run it:
|
||||
|
||||
```sh
|
||||
lune script-name
|
||||
```
|
||||
|
||||
This will look for the script `script-name` in a few locations:
|
||||
|
||||
- The current directory
|
||||
- The folder `lune` in the current directory, if it exists
|
||||
- The folder `.lune` in the current directory, if it exists
|
||||
|
||||
If you don't want Lune to look in sub-directories you can provide a full file path with the file extension included, instead of only the file name.
|
||||
Head over to the [Installation](https://lune-org.github.io/docs/getting-started/1-installation) page to get started using Lune!
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
[tools]
|
||||
luau-lsp = "JohnnyMorganz/luau-lsp@1.15.0"
|
||||
selene = "Kampfkarren/selene@0.24.0"
|
||||
stylua = "JohnnyMorganz/StyLua@0.16.0"
|
BIN
assets/logo/no-tilt-grid.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
assets/logo/no-tilt-grid.png
Normal file
After Width: | Height: | Size: 148 KiB |
69
assets/logo/no-tilt-grid_svg.svg
Normal file
|
@ -0,0 +1,69 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_i_6_19)">
|
||||
<g clip-path="url(#clip0_6_19)">
|
||||
<rect x="2" y="2" width="16" height="16" rx="1" fill="url(#paint0_linear_6_19)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.875 0.125L18.125 0.125V1.875H19.875V0.125ZM19.875 2.125H18.125V3.875H19.875V2.125ZM19.875 4.125H18.125V5.875H19.875V4.125ZM19.875 6.125H18.125V7.875H19.875V6.125ZM19.875 8.125H18.125V9.875H19.875V8.125ZM19.875 10.125H18.125V11.875H19.875V10.125ZM19.875 12.125H18.125V13.875H19.875V12.125ZM19.875 14.125H18.125V15.875H19.875V14.125ZM19.875 16.125H18.125V17.875H19.875V16.125ZM19.875 18.125H18.125V19.875H19.875V18.125ZM20 20V20.125H4.76837e-07V20H-0.125L-0.125 -5.21541e-07L4.76837e-07 -5.1409e-07V-0.125L20 -0.125V2.92063e-06H20.125V20H20ZM0.125 0.125L0.125 1.875H1.87499L1.87499 0.125L0.125 0.125ZM2.12499 0.125V1.875H3.87499V0.125L2.12499 0.125ZM4.12499 0.125V1.875H5.87499V0.125L4.12499 0.125ZM6.12499 0.125V1.875H7.87499V0.125L6.12499 0.125ZM8.12499 0.125V1.875H9.87499V0.125L8.12499 0.125ZM10.125 0.125V1.875H11.875V0.125L10.125 0.125ZM12.125 0.125V1.875H13.875V0.125L12.125 0.125ZM14.125 0.125V1.875H15.875V0.125L14.125 0.125ZM16.125 0.125V1.875H17.875V0.125L16.125 0.125ZM17.875 2.125H16.125V3.875H17.875V2.125ZM17.875 4.125H16.125V5.875H17.875V4.125ZM17.875 6.125H16.125V7.875H17.875V6.125ZM17.875 8.125H16.125V9.875H17.875V8.125ZM17.875 10.125H16.125V11.875H17.875V10.125ZM17.875 12.125H16.125V13.875H17.875V12.125ZM17.875 14.125H16.125V15.875H17.875V14.125ZM17.875 16.125H16.125V17.875H17.875V16.125ZM17.875 18.125H16.125V19.875H17.875V18.125ZM15.875 19.875V18.125H14.125V19.875H15.875ZM13.875 19.875V18.125H12.125V19.875H13.875ZM11.875 19.875V18.125H10.125V19.875H11.875ZM9.87499 19.875V18.125H8.12499V19.875H9.87499ZM7.87499 19.875V18.125H6.12499V19.875H7.87499ZM5.87499 19.875V18.125H4.12499V19.875H5.87499ZM3.87499 19.875V18.125H2.12499V19.875H3.87499ZM1.87499 19.875L1.87499 18.125H0.124999L0.124999 19.875H1.87499ZM0.124999 17.875H1.87499V16.125H0.124999L0.124999 17.875ZM0.124999 15.875H1.87499L1.87499 14.125H0.124999L0.124999 15.875ZM0.124999 13.875H1.87499L1.87499 12.125H0.124999L0.124999 13.875ZM0.124999 11.875H1.87499L1.87499 10.125H0.125L0.124999 11.875ZM0.125 9.875H1.87499V8.125H0.125L0.125 9.875ZM0.125 7.875H1.87499L1.87499 6.125H0.125L0.125 7.875ZM0.125 5.875H1.87499L1.87499 4.125H0.125L0.125 5.875ZM0.125 3.875H1.87499V2.125H0.125L0.125 3.875ZM2.12499 2.125L2.12499 3.875H3.87499L3.87499 2.125H2.12499ZM4.12499 2.125V3.875H5.87499V2.125H4.12499ZM6.12499 2.125V3.875H7.87499V2.125H6.12499ZM8.12499 2.125V3.875H9.87499V2.125H8.12499ZM10.125 2.125V3.875H11.875V2.125H10.125ZM12.125 2.125V3.875H13.875V2.125H12.125ZM14.125 2.125V3.875H15.875V2.125H14.125ZM15.875 4.125H14.125V5.875H15.875V4.125ZM15.875 6.125H14.125V7.875H15.875V6.125ZM15.875 8.125H14.125V9.875H15.875V8.125ZM15.875 10.125H14.125V11.875H15.875V10.125ZM15.875 12.125H14.125V13.875H15.875V12.125ZM15.875 14.125H14.125V15.875H15.875V14.125ZM15.875 16.125H14.125V17.875H15.875V16.125ZM13.875 17.875V16.125H12.125V17.875H13.875ZM11.875 17.875V16.125H10.125V17.875H11.875ZM9.87499 17.875V16.125H8.12499V17.875H9.87499ZM7.87499 17.875V16.125H6.12499V17.875H7.87499ZM5.87499 17.875V16.125H4.12499V17.875H5.87499ZM3.87499 17.875L3.87499 16.125H2.12499L2.12499 17.875H3.87499ZM2.12499 15.875H3.87499V14.125H2.12499V15.875ZM2.12499 13.875H3.87499L3.87499 12.125H2.12499L2.12499 13.875ZM2.12499 11.875H3.87499V10.125H2.12499V11.875ZM2.12499 9.875H3.87499V8.125H2.12499V9.875ZM2.12499 7.875H3.87499L3.87499 6.125H2.12499L2.12499 7.875ZM2.12499 5.875H3.87499V4.125H2.12499V5.875ZM4.12499 4.125L4.12499 5.875H5.87499L5.87499 4.125H4.12499ZM6.12499 4.125L6.12499 5.875H7.87499L7.87499 4.125H6.12499ZM8.12499 4.125V5.875H9.87499V4.125H8.12499ZM10.125 4.125V5.875H11.875V4.125H10.125ZM12.125 4.125V5.875H13.875V4.125H12.125ZM13.875 6.125H12.125V7.875H13.875V6.125ZM13.875 8.125H12.125V9.875H13.875V8.125ZM13.875 10.125H12.125V11.875H13.875V10.125ZM13.875 12.125H12.125V13.875H13.875V12.125ZM13.875 14.125H12.125V15.875H13.875V14.125ZM11.875 15.875V14.125H10.125V15.875H11.875ZM9.87499 15.875V14.125H8.12499V15.875H9.87499ZM7.87499 15.875L7.87499 14.125H6.12499L6.12499 15.875H7.87499ZM5.87499 15.875L5.87499 14.125H4.12499L4.12499 15.875H5.87499ZM4.12499 13.875H5.87499V12.125H4.12499V13.875ZM4.12499 11.875H5.87499V10.125H4.12499V11.875ZM4.12499 9.875H5.87499V8.125H4.12499V9.875ZM4.12499 7.875H5.87499V6.125H4.12499V7.875ZM6.12499 6.125V7.875H7.87499V6.125H6.12499ZM8.12499 6.125V7.875H9.87499V6.125H8.12499ZM10.125 6.125V7.875H11.875V6.125H10.125ZM11.875 8.125H10.125V9.875H11.875V8.125ZM11.875 10.125H10.125V11.875H11.875V10.125ZM11.875 12.125H10.125V13.875H11.875V12.125ZM9.87499 13.875V12.125H8.12499V13.875H9.87499ZM7.87499 13.875V12.125H6.12499V13.875H7.87499ZM6.12499 11.875H7.87499V10.125H6.12499V11.875ZM6.12499 9.875H7.87499V8.125H6.12499V9.875ZM8.12499 8.125V9.875H9.87499V8.125H8.12499ZM9.87499 10.125H8.12499V11.875H9.87499V10.125Z" fill="url(#paint1_linear_6_19)" fill-opacity="0.05"/>
|
||||
<g filter="url(#filter1_di_6_19)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.375 6.25C14.2725 6.25 15 5.52246 15 4.625C15 4.4376 14.9683 4.2576 14.9099 4.09009C15.5446 4.31125 16 4.91494 16 5.625C16 6.52246 15.2725 7.25 14.375 7.25C13.6649 7.25 13.0613 6.79458 12.8401 6.15991C13.0076 6.21828 13.1876 6.25 13.375 6.25Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_di_6_19)">
|
||||
<path d="M6.625 15.54V16H4.49219V15.54H6.625ZM4.60352 11.7344V16H4.03809V11.7344H4.60352ZM9.00098 15.2676V12.8301H9.5459V16H9.02734L9.00098 15.2676ZM9.10352 14.5996L9.3291 14.5938C9.3291 14.8047 9.30664 15 9.26172 15.1797C9.21875 15.3574 9.14844 15.5117 9.05078 15.6426C8.95312 15.7734 8.8252 15.876 8.66699 15.9502C8.50879 16.0225 8.31641 16.0586 8.08984 16.0586C7.93555 16.0586 7.79395 16.0361 7.66504 15.9912C7.53809 15.9463 7.42871 15.877 7.33691 15.7832C7.24512 15.6895 7.17383 15.5674 7.12305 15.417C7.07422 15.2666 7.0498 15.0859 7.0498 14.875V12.8301H7.5918V14.8809C7.5918 15.0234 7.60742 15.1416 7.63867 15.2354C7.67188 15.3271 7.71582 15.4004 7.77051 15.4551C7.82715 15.5078 7.88965 15.5449 7.95801 15.5664C8.02832 15.5879 8.10059 15.5986 8.1748 15.5986C8.40527 15.5986 8.58789 15.5547 8.72266 15.4668C8.85742 15.377 8.9541 15.2568 9.0127 15.1064C9.07324 14.9541 9.10352 14.7852 9.10352 14.5996ZM10.9141 13.5068V16H10.3721V12.8301H10.8848L10.9141 13.5068ZM10.7852 14.2949L10.5596 14.2861C10.5615 14.0693 10.5938 13.8691 10.6562 13.6855C10.7188 13.5 10.8066 13.3389 10.9199 13.2021C11.0332 13.0654 11.168 12.96 11.3242 12.8857C11.4824 12.8096 11.6572 12.7715 11.8486 12.7715C12.0049 12.7715 12.1455 12.793 12.2705 12.8359C12.3955 12.877 12.502 12.9434 12.5898 13.0352C12.6797 13.127 12.748 13.2461 12.7949 13.3926C12.8418 13.5371 12.8652 13.7139 12.8652 13.9229V16H12.3203V13.917C12.3203 13.751 12.2959 13.6182 12.2471 13.5186C12.1982 13.417 12.127 13.3438 12.0332 13.2988C11.9395 13.252 11.8242 13.2285 11.6875 13.2285C11.5527 13.2285 11.4297 13.2568 11.3184 13.3135C11.209 13.3701 11.1143 13.4482 11.0342 13.5479C10.9561 13.6475 10.8945 13.7617 10.8496 13.8906C10.8066 14.0176 10.7852 14.1523 10.7852 14.2949ZM15.0039 16.0586C14.7832 16.0586 14.583 16.0215 14.4033 15.9473C14.2256 15.8711 14.0723 15.7646 13.9434 15.6279C13.8164 15.4912 13.7188 15.3291 13.6504 15.1416C13.582 14.9541 13.5479 14.749 13.5479 14.5264V14.4033C13.5479 14.1455 13.5859 13.916 13.6621 13.7148C13.7383 13.5117 13.8418 13.3398 13.9727 13.1992C14.1035 13.0586 14.252 12.9521 14.418 12.8799C14.584 12.8076 14.7559 12.7715 14.9336 12.7715C15.1602 12.7715 15.3555 12.8105 15.5195 12.8887C15.6855 12.9668 15.8213 13.0762 15.9268 13.2168C16.0322 13.3555 16.1104 13.5195 16.1611 13.709C16.2119 13.8965 16.2373 14.1016 16.2373 14.3242V14.5674H13.8701V14.125H15.6953V14.084C15.6875 13.9434 15.6582 13.8066 15.6074 13.6738C15.5586 13.541 15.4805 13.4316 15.373 13.3457C15.2656 13.2598 15.1191 13.2168 14.9336 13.2168C14.8105 13.2168 14.6973 13.2432 14.5938 13.2959C14.4902 13.3467 14.4014 13.4229 14.3271 13.5244C14.2529 13.626 14.1953 13.75 14.1543 13.8965C14.1133 14.043 14.0928 14.2119 14.0928 14.4033V14.5264C14.0928 14.6768 14.1133 14.8184 14.1543 14.9512C14.1973 15.082 14.2588 15.1973 14.3389 15.2969C14.4209 15.3965 14.5195 15.4746 14.6348 15.5312C14.752 15.5879 14.8848 15.6162 15.0332 15.6162C15.2246 15.6162 15.3867 15.5771 15.5195 15.499C15.6523 15.4209 15.7686 15.3164 15.8682 15.1855L16.1963 15.4463C16.1279 15.5498 16.041 15.6484 15.9355 15.7422C15.8301 15.8359 15.7002 15.9121 15.5459 15.9707C15.3936 16.0293 15.2129 16.0586 15.0039 16.0586Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_6_19" x="2" y="1.5" width="16.5" height="16.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.5" dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_6_19"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_6_19" x="10.8401" y="3.09009" width="6.15991" height="6.15991" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_19"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_19" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_19"/>
|
||||
</filter>
|
||||
<filter id="filter2_di_6_19" x="2.03809" y="10.7344" width="15.1992" height="7.32422" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_19"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_19" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_19"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_6_19" x1="10" y1="2" x2="10" y2="18" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E168FF"/>
|
||||
<stop offset="1" stop-color="#C848E9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_6_19" x1="10" y1="-0.125" x2="10" y2="20.125" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.5"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6_19">
|
||||
<rect x="2" y="2" width="16" height="16" rx="1" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/logo/no-tilt.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
assets/logo/no-tilt.png
Normal file
After Width: | Height: | Size: 138 KiB |
64
assets/logo/no-tilt_svg.svg
Normal file
|
@ -0,0 +1,64 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_i_6_67)">
|
||||
<g clip-path="url(#clip0_6_67)">
|
||||
<rect x="2" y="2" width="16" height="16" rx="1" fill="url(#paint0_linear_6_67)"/>
|
||||
<g filter="url(#filter1_di_6_67)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.375 6.25C14.2725 6.25 15 5.52246 15 4.625C15 4.43759 14.9683 4.2576 14.9099 4.09009C15.5446 4.31125 16 4.91494 16 5.625C16 6.52246 15.2725 7.25 14.375 7.25C13.6649 7.25 13.0613 6.79458 12.8401 6.15991C13.0076 6.21828 13.1876 6.25 13.375 6.25Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_di_6_67)">
|
||||
<path d="M6.625 15.54V16H4.49219V15.54H6.625ZM4.60352 11.7344V16H4.03809V11.7344H4.60352ZM9.00098 15.2676V12.8301H9.5459V16H9.02734L9.00098 15.2676ZM9.10352 14.5996L9.3291 14.5938C9.3291 14.8047 9.30664 15 9.26172 15.1797C9.21875 15.3574 9.14844 15.5117 9.05078 15.6426C8.95312 15.7734 8.8252 15.876 8.66699 15.9502C8.50879 16.0225 8.31641 16.0586 8.08984 16.0586C7.93555 16.0586 7.79395 16.0361 7.66504 15.9912C7.53809 15.9463 7.42871 15.877 7.33691 15.7832C7.24512 15.6895 7.17383 15.5674 7.12305 15.417C7.07422 15.2666 7.0498 15.0859 7.0498 14.875V12.8301H7.5918V14.8809C7.5918 15.0234 7.60742 15.1416 7.63867 15.2354C7.67188 15.3271 7.71582 15.4004 7.77051 15.4551C7.82715 15.5078 7.88965 15.5449 7.95801 15.5664C8.02832 15.5879 8.10059 15.5986 8.1748 15.5986C8.40527 15.5986 8.58789 15.5547 8.72266 15.4668C8.85742 15.377 8.9541 15.2568 9.0127 15.1064C9.07324 14.9541 9.10352 14.7852 9.10352 14.5996ZM10.9141 13.5068V16H10.3721V12.8301H10.8848L10.9141 13.5068ZM10.7852 14.2949L10.5596 14.2861C10.5615 14.0693 10.5938 13.8691 10.6562 13.6855C10.7188 13.5 10.8066 13.3389 10.9199 13.2021C11.0332 13.0654 11.168 12.96 11.3242 12.8857C11.4824 12.8096 11.6572 12.7715 11.8486 12.7715C12.0049 12.7715 12.1455 12.793 12.2705 12.8359C12.3955 12.877 12.502 12.9434 12.5898 13.0352C12.6797 13.127 12.748 13.2461 12.7949 13.3926C12.8418 13.5371 12.8652 13.7139 12.8652 13.9229V16H12.3203V13.917C12.3203 13.751 12.2959 13.6182 12.2471 13.5186C12.1982 13.417 12.127 13.3438 12.0332 13.2988C11.9395 13.252 11.8242 13.2285 11.6875 13.2285C11.5527 13.2285 11.4297 13.2568 11.3184 13.3135C11.209 13.3701 11.1143 13.4482 11.0342 13.5479C10.9561 13.6475 10.8945 13.7617 10.8496 13.8906C10.8066 14.0176 10.7852 14.1523 10.7852 14.2949ZM15.0039 16.0586C14.7832 16.0586 14.583 16.0215 14.4033 15.9473C14.2256 15.8711 14.0723 15.7646 13.9434 15.6279C13.8164 15.4912 13.7188 15.3291 13.6504 15.1416C13.582 14.9541 13.5479 14.749 13.5479 14.5264V14.4033C13.5479 14.1455 13.5859 13.916 13.6621 13.7148C13.7383 13.5117 13.8418 13.3398 13.9727 13.1992C14.1035 13.0586 14.252 12.9521 14.418 12.8799C14.584 12.8076 14.7559 12.7715 14.9336 12.7715C15.1602 12.7715 15.3555 12.8105 15.5195 12.8887C15.6855 12.9668 15.8213 13.0762 15.9268 13.2168C16.0322 13.3555 16.1104 13.5195 16.1611 13.709C16.2119 13.8965 16.2373 14.1016 16.2373 14.3242V14.5674H13.8701V14.125H15.6953V14.084C15.6875 13.9434 15.6582 13.8066 15.6074 13.6738C15.5586 13.541 15.4805 13.4316 15.373 13.3457C15.2656 13.2598 15.1191 13.2168 14.9336 13.2168C14.8105 13.2168 14.6973 13.2432 14.5938 13.2959C14.4902 13.3467 14.4014 13.4229 14.3271 13.5244C14.2529 13.626 14.1953 13.75 14.1543 13.8965C14.1133 14.043 14.0928 14.2119 14.0928 14.4033V14.5264C14.0928 14.6768 14.1133 14.8184 14.1543 14.9512C14.1973 15.082 14.2588 15.1973 14.3389 15.2969C14.4209 15.3965 14.5195 15.4746 14.6348 15.5312C14.752 15.5879 14.8848 15.6162 15.0332 15.6162C15.2246 15.6162 15.3867 15.5771 15.5195 15.499C15.6523 15.4209 15.7686 15.3164 15.8682 15.1855L16.1963 15.4463C16.1279 15.5498 16.041 15.6484 15.9355 15.7422C15.8301 15.8359 15.7002 15.9121 15.5459 15.9707C15.3936 16.0293 15.2129 16.0586 15.0039 16.0586Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_6_67" x="2" y="1.5" width="16.5" height="16.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.5" dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_6_67"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_6_67" x="10.8401" y="3.09009" width="6.15991" height="6.15991" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_67"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_67" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_67"/>
|
||||
</filter>
|
||||
<filter id="filter2_di_6_67" x="2.03809" y="10.7344" width="15.1992" height="7.32422" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_67"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_67" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_67"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_6_67" x1="10" y1="2" x2="10" y2="18" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E168FF"/>
|
||||
<stop offset="1" stop-color="#C848E9"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6_67">
|
||||
<rect x="2" y="2" width="16" height="16" rx="1" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 6.9 KiB |
BIN
assets/logo/tilt-grid.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
assets/logo/tilt-grid.png
Normal file
After Width: | Height: | Size: 180 KiB |
74
assets/logo/tilt-grid_svg.svg
Normal file
|
@ -0,0 +1,74 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_97)">
|
||||
<g filter="url(#filter0_i_6_97)">
|
||||
<g clip-path="url(#clip1_6_97)">
|
||||
<rect x="4.34315" y="0.202042" width="16" height="16" rx="1" transform="rotate(15 4.34315 0.202042)" fill="url(#paint0_linear_6_97)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.875 0.125H18.125V1.875H19.875V0.125ZM19.875 2.125H18.125V3.875H19.875V2.125ZM19.875 4.125H18.125V5.875H19.875V4.125ZM19.875 6.125H18.125V7.875H19.875V6.125ZM19.875 8.125H18.125V9.875H19.875V8.125ZM19.875 10.125H18.125V11.875H19.875V10.125ZM19.875 12.125H18.125V13.875H19.875V12.125ZM19.875 14.125H18.125V15.875H19.875V14.125ZM19.875 16.125H18.125V17.875H19.875V16.125ZM19.875 18.125H18.125V19.875H19.875V18.125ZM20 20V20.125H4.76837e-07V20H-0.125V-5.21541e-07L4.76837e-07 -5.1409e-07V-0.125H20V2.92063e-06H20.125V20H20ZM0.125 0.125L0.125 1.875H1.87499L1.87499 0.125H0.125ZM2.12499 0.125V1.875H3.87499V0.125H2.12499ZM4.12499 0.125V1.875H5.87499V0.125H4.12499ZM6.12499 0.125V1.875H7.87499V0.125H6.12499ZM8.12499 0.125V1.875H9.87499V0.125H8.12499ZM10.125 0.125V1.875H11.875V0.125H10.125ZM12.125 0.125V1.875H13.875V0.125H12.125ZM14.125 0.125V1.875H15.875V0.125H14.125ZM16.125 0.125V1.875H17.875V0.125H16.125ZM17.875 2.125H16.125V3.875H17.875V2.125ZM17.875 4.125H16.125V5.875H17.875V4.125ZM17.875 6.125H16.125V7.875H17.875V6.125ZM17.875 8.125H16.125V9.875H17.875V8.125ZM17.875 10.125H16.125V11.875H17.875V10.125ZM17.875 12.125H16.125V13.875H17.875V12.125ZM17.875 14.125H16.125V15.875H17.875V14.125ZM17.875 16.125H16.125V17.875H17.875V16.125ZM17.875 18.125H16.125V19.875H17.875V18.125ZM15.875 19.875V18.125H14.125V19.875H15.875ZM13.875 19.875V18.125H12.125V19.875H13.875ZM11.875 19.875V18.125H10.125V19.875H11.875ZM9.87499 19.875V18.125H8.12499V19.875H9.87499ZM7.87499 19.875V18.125H6.12499V19.875H7.87499ZM5.87499 19.875V18.125H4.12499V19.875H5.87499ZM3.87499 19.875V18.125H2.12499V19.875H3.87499ZM1.87499 19.875L1.87499 18.125H0.124999L0.124999 19.875H1.87499ZM0.124999 17.875H1.87499V16.125H0.124999L0.124999 17.875ZM0.124999 15.875H1.87499L1.87499 14.125H0.124999L0.124999 15.875ZM0.124999 13.875H1.87499L1.87499 12.125H0.124999L0.124999 13.875ZM0.124999 11.875H1.87499V10.125H0.125L0.124999 11.875ZM0.125 9.875H1.87499V8.125H0.125L0.125 9.875ZM0.125 7.875H1.87499L1.87499 6.125H0.125L0.125 7.875ZM0.125 5.875H1.87499L1.87499 4.125H0.125L0.125 5.875ZM0.125 3.875H1.87499V2.125H0.125L0.125 3.875ZM2.12499 2.125L2.12499 3.875H3.87499L3.87499 2.125H2.12499ZM4.12499 2.125V3.875H5.87499V2.125H4.12499ZM6.12499 2.125V3.875H7.87499V2.125H6.12499ZM8.12499 2.125V3.875H9.87499V2.125H8.12499ZM10.125 2.125V3.875H11.875V2.125H10.125ZM12.125 2.125V3.875H13.875V2.125H12.125ZM14.125 2.125V3.875H15.875V2.125H14.125ZM15.875 4.125H14.125V5.875H15.875V4.125ZM15.875 6.125H14.125V7.875H15.875V6.125ZM15.875 8.125H14.125V9.875H15.875V8.125ZM15.875 10.125H14.125V11.875H15.875V10.125ZM15.875 12.125H14.125V13.875H15.875V12.125ZM15.875 14.125H14.125V15.875H15.875V14.125ZM15.875 16.125H14.125V17.875H15.875V16.125ZM13.875 17.875V16.125H12.125V17.875H13.875ZM11.875 17.875V16.125H10.125V17.875H11.875ZM9.87499 17.875V16.125H8.12499V17.875H9.87499ZM7.87499 17.875V16.125H6.12499V17.875H7.87499ZM5.87499 17.875V16.125H4.12499V17.875H5.87499ZM3.87499 17.875L3.87499 16.125H2.12499L2.12499 17.875H3.87499ZM2.12499 15.875H3.87499V14.125H2.12499V15.875ZM2.12499 13.875H3.87499L3.87499 12.125H2.12499L2.12499 13.875ZM2.12499 11.875H3.87499V10.125H2.12499V11.875ZM2.12499 9.875H3.87499V8.125H2.12499V9.875ZM2.12499 7.875H3.87499L3.87499 6.125H2.12499L2.12499 7.875ZM2.12499 5.875H3.87499V4.125H2.12499V5.875ZM4.12499 4.125L4.12499 5.875H5.87499L5.87499 4.125H4.12499ZM6.12499 4.125L6.12499 5.875H7.87499L7.87499 4.125H6.12499ZM8.12499 4.125V5.875H9.87499V4.125H8.12499ZM10.125 4.125V5.875H11.875V4.125H10.125ZM12.125 4.125V5.875H13.875V4.125H12.125ZM13.875 6.125H12.125V7.875H13.875V6.125ZM13.875 8.125H12.125V9.875H13.875V8.125ZM13.875 10.125H12.125V11.875H13.875V10.125ZM13.875 12.125H12.125V13.875H13.875V12.125ZM13.875 14.125H12.125V15.875H13.875V14.125ZM11.875 15.875V14.125H10.125V15.875H11.875ZM9.87499 15.875V14.125H8.12499V15.875H9.87499ZM7.87499 15.875L7.87499 14.125H6.12499L6.12499 15.875H7.87499ZM5.87499 15.875L5.87499 14.125H4.12499L4.12499 15.875H5.87499ZM4.12499 13.875H5.87499V12.125H4.12499V13.875ZM4.12499 11.875H5.87499V10.125H4.12499V11.875ZM4.12499 9.875H5.87499V8.125H4.12499V9.875ZM4.12499 7.875H5.87499V6.125H4.12499V7.875ZM6.12499 6.125V7.875H7.87499V6.125H6.12499ZM8.12499 6.125V7.875H9.87499V6.125H8.12499ZM10.125 6.125V7.875H11.875V6.125H10.125ZM11.875 8.125H10.125V9.875H11.875V8.125ZM11.875 10.125H10.125V11.875H11.875V10.125ZM11.875 12.125H10.125V13.875H11.875V12.125ZM9.87499 13.875V12.125H8.12499V13.875H9.87499ZM7.87499 13.875V12.125H6.12499V13.875H7.87499ZM6.12499 11.875H7.87499V10.125H6.12499V11.875ZM6.12499 9.875H7.87499V8.125H6.12499V9.875ZM8.12499 8.125V9.875H9.87499V8.125H8.12499ZM9.87499 10.125H8.12499V11.875H9.87499V10.125Z" fill="url(#paint1_linear_6_97)" fill-opacity="0.05"/>
|
||||
<g filter="url(#filter1_di_6_97)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2306 7.25129C15.0975 7.48357 15.9885 6.96913 16.2208 6.10224C16.2693 5.92123 16.2852 5.73915 16.2722 5.56225C16.828 5.94013 17.1117 6.64112 16.9279 7.32699C16.6956 8.19387 15.8046 8.70832 14.9377 8.47604C14.2518 8.29226 13.7866 7.69611 13.7372 7.02583C13.8839 7.12556 14.0496 7.20279 14.2306 7.25129Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_di_6_97)">
|
||||
<path d="M5.30613 14.4778L5.18709 14.922L3.12695 14.37L3.24599 13.9257L5.30613 14.4778ZM4.33851 10.2786L3.23448 14.3988L2.68832 14.2525L3.79234 10.1322L4.33851 10.2786ZM7.67167 14.8295L8.30254 12.4751L8.82889 12.6161L8.00846 15.678L7.50757 15.5438L7.67167 14.8295ZM7.9436 14.2109L8.16301 14.2636C8.10842 14.4673 8.03617 14.6502 7.94627 14.8121C7.85877 14.9727 7.75092 15.1035 7.62272 15.2046C7.49452 15.3058 7.34441 15.3717 7.17239 15.4024C7.00087 15.4313 6.80569 15.4164 6.58685 15.3578C6.43781 15.3178 6.30685 15.2595 6.19396 15.1827C6.08296 15.1065 5.99526 15.0112 5.93085 14.8969C5.86645 14.7826 5.82918 14.6462 5.81906 14.4878C5.81081 14.3299 5.83399 14.1491 5.88859 13.9453L6.41785 11.9701L6.94138 12.1104L6.41059 14.0913C6.37369 14.229 6.3582 14.3472 6.36412 14.4458C6.37244 14.5431 6.39593 14.6252 6.4346 14.6922C6.47566 14.7578 6.52642 14.8098 6.58689 14.8482C6.64925 14.8872 6.71627 14.9163 6.78796 14.9355C7.01058 14.9951 7.19835 14.9999 7.35127 14.9499C7.5047 14.898 7.62917 14.807 7.72469 14.6769C7.8226 14.5454 7.89557 14.3901 7.9436 14.2109ZM9.97528 13.6239L9.33 16.0321L8.80648 15.8919L9.62691 12.8299L10.1221 12.9626L9.97528 13.6239ZM9.64679 14.3518L9.43117 14.2849C9.48917 14.076 9.57211 13.891 9.68 13.7298C9.78839 13.5668 9.91499 13.4339 10.0598 13.3311C10.2046 13.2284 10.3621 13.1614 10.5322 13.1301C10.7047 13.0975 10.8834 13.106 11.0683 13.1555C11.2193 13.1959 11.3495 13.2531 11.4591 13.327C11.5693 13.3989 11.6549 13.4906 11.716 13.602C11.7791 13.714 11.8143 13.8467 11.8216 14.0004C11.8295 14.1521 11.8064 14.3289 11.7523 14.5308L11.2147 16.5371L10.6883 16.3961L11.2275 14.3841C11.2704 14.2237 11.2812 14.0891 11.2598 13.9802C11.239 13.8695 11.1891 13.7803 11.1101 13.7127C11.0317 13.6431 10.9265 13.5906 10.7944 13.5553C10.6642 13.5204 10.538 13.5159 10.4158 13.5418C10.2955 13.5682 10.1838 13.6191 10.0807 13.6946C9.97944 13.7706 9.89045 13.8651 9.81369 13.9779C9.73933 14.0895 9.6837 14.2141 9.64679 14.3518ZM13.2653 17.1473C13.0521 17.0901 12.8684 17.0025 12.714 16.8843C12.5621 16.7647 12.4415 16.6222 12.3524 16.4568C12.2651 16.2919 12.2128 16.11 12.1953 15.9112C12.1778 15.7124 12.1978 15.5055 12.2555 15.2904L12.2873 15.1715C12.354 14.9225 12.4502 14.7107 12.5759 14.5361C12.702 14.3596 12.8465 14.2204 13.0093 14.1184C13.1721 14.0164 13.343 13.952 13.5221 13.9252C13.7011 13.8984 13.8765 13.908 14.0482 13.954C14.267 14.0126 14.4456 14.1009 14.5838 14.2188C14.724 14.3372 14.8268 14.478 14.8922 14.6411C14.9582 14.8024 14.9912 14.9811 14.9912 15.1772C14.9918 15.3715 14.9632 15.5761 14.9056 15.7912L14.8426 16.0261L12.5561 15.4134L12.6706 14.9861L14.4336 15.4585L14.4442 15.4189C14.4731 15.281 14.4802 15.1414 14.4655 15C14.4527 14.859 14.4056 14.7332 14.324 14.6223C14.2425 14.5115 14.1121 14.4321 13.9329 14.3841C13.8141 14.3522 13.6978 14.3484 13.5842 14.3725C13.471 14.3948 13.3655 14.4454 13.2675 14.5243C13.1695 14.6032 13.0818 14.708 13.0043 14.8389C12.9267 14.9698 12.8632 15.1277 12.8137 15.3126L12.7818 15.4314C12.7429 15.5767 12.726 15.7188 12.7313 15.8577C12.7389 15.9952 12.7685 16.1224 12.8201 16.2394C12.8735 16.3568 12.9486 16.4578 13.0452 16.5423C13.1438 16.6274 13.2647 16.6891 13.4081 16.7275C13.593 16.7771 13.7597 16.7813 13.9082 16.7402C14.0567 16.6991 14.196 16.6283 14.3261 16.5276L14.5756 16.8644C14.4827 16.9467 14.3732 17.0195 14.2471 17.0828C14.121 17.146 13.9758 17.186 13.8116 17.2026C13.6493 17.2198 13.4672 17.2013 13.2653 17.1473Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_6_97" x="0.202041" y="-0.297958" width="20.0959" height="20.0959" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.5" dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_6_97"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_6_97" x="11.7372" y="4.56225" width="6.24645" height="5.96956" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_97"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_97" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_97"/>
|
||||
</filter>
|
||||
<filter id="filter2_di_6_97" x="0.673153" y="9.13222" width="15.9027" height="10.3343" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_97"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_97" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_97"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_6_97" x1="12.3431" y1="0.202042" x2="12.3431" y2="16.202" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E168FF"/>
|
||||
<stop offset="1" stop-color="#C848E9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_6_97" x1="10" y1="-0.125" x2="10" y2="20.125" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white" stop-opacity="0.5"/>
|
||||
<stop offset="1" stop-color="white"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6_97">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_6_97">
|
||||
<rect x="4.34315" y="0.202042" width="16" height="16" rx="1" transform="rotate(15 4.34315 0.202042)" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/logo/tilt.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
assets/logo/tilt.png
Normal file
After Width: | Height: | Size: 172 KiB |
69
assets/logo/tilt_svg.svg
Normal file
|
@ -0,0 +1,69 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6_77)">
|
||||
<g filter="url(#filter0_i_6_77)">
|
||||
<g clip-path="url(#clip1_6_77)">
|
||||
<rect x="4.34315" y="0.202042" width="16" height="16" rx="1" transform="rotate(15 4.34315 0.202042)" fill="url(#paint0_linear_6_77)"/>
|
||||
<g filter="url(#filter1_di_6_77)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.2306 7.25129C15.0975 7.48357 15.9885 6.96913 16.2208 6.10224C16.2693 5.92123 16.2852 5.73915 16.2722 5.56225C16.828 5.94013 17.1117 6.64112 16.9279 7.32699C16.6956 8.19387 15.8046 8.70832 14.9377 8.47604C14.2518 8.29226 13.7866 7.69611 13.7372 7.02583C13.8839 7.12556 14.0496 7.20279 14.2306 7.25129Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_di_6_77)">
|
||||
<path d="M5.30613 14.4778L5.18709 14.922L3.12695 14.37L3.24599 13.9257L5.30613 14.4778ZM4.33851 10.2786L3.23448 14.3988L2.68832 14.2525L3.79234 10.1322L4.33851 10.2786ZM7.67167 14.8295L8.30254 12.4751L8.82889 12.6161L8.00846 15.678L7.50757 15.5438L7.67167 14.8295ZM7.9436 14.2109L8.16301 14.2636C8.10842 14.4673 8.03617 14.6502 7.94627 14.8121C7.85877 14.9727 7.75092 15.1035 7.62272 15.2046C7.49452 15.3058 7.34441 15.3717 7.17239 15.4024C7.00087 15.4313 6.80569 15.4164 6.58685 15.3578C6.43781 15.3178 6.30685 15.2595 6.19396 15.1827C6.08296 15.1065 5.99526 15.0112 5.93085 14.8969C5.86645 14.7826 5.82918 14.6462 5.81906 14.4878C5.81081 14.3299 5.83399 14.1491 5.88859 13.9453L6.41785 11.9701L6.94138 12.1104L6.41059 14.0913C6.37369 14.229 6.3582 14.3472 6.36412 14.4458C6.37244 14.5431 6.39593 14.6252 6.4346 14.6922C6.47566 14.7578 6.52642 14.8098 6.58689 14.8482C6.64925 14.8872 6.71627 14.9163 6.78796 14.9355C7.01058 14.9951 7.19835 14.9999 7.35127 14.9499C7.5047 14.898 7.62917 14.807 7.72469 14.6769C7.8226 14.5454 7.89557 14.3901 7.9436 14.2109ZM9.97528 13.6239L9.33 16.0321L8.80648 15.8919L9.62691 12.8299L10.1221 12.9626L9.97528 13.6239ZM9.64679 14.3518L9.43117 14.2849C9.48917 14.076 9.57211 13.891 9.68 13.7298C9.78839 13.5668 9.91499 13.4339 10.0598 13.3311C10.2046 13.2284 10.3621 13.1614 10.5322 13.1301C10.7047 13.0975 10.8834 13.106 11.0683 13.1555C11.2193 13.1959 11.3495 13.2531 11.4591 13.327C11.5693 13.3989 11.6549 13.4906 11.716 13.602C11.7791 13.714 11.8143 13.8467 11.8216 14.0004C11.8295 14.1521 11.8064 14.3289 11.7523 14.5308L11.2147 16.5371L10.6883 16.3961L11.2275 14.3841C11.2704 14.2237 11.2812 14.0891 11.2598 13.9802C11.239 13.8695 11.1891 13.7803 11.1101 13.7127C11.0317 13.6431 10.9265 13.5906 10.7944 13.5553C10.6642 13.5204 10.538 13.5159 10.4158 13.5418C10.2955 13.5682 10.1838 13.6191 10.0807 13.6946C9.97944 13.7706 9.89045 13.8651 9.81369 13.9779C9.73933 14.0895 9.6837 14.2141 9.64679 14.3518ZM13.2653 17.1473C13.0521 17.0901 12.8684 17.0025 12.714 16.8843C12.5621 16.7647 12.4415 16.6222 12.3524 16.4568C12.2651 16.2919 12.2128 16.11 12.1953 15.9112C12.1778 15.7124 12.1978 15.5055 12.2555 15.2904L12.2873 15.1715C12.354 14.9225 12.4502 14.7107 12.5759 14.5361C12.702 14.3596 12.8465 14.2204 13.0093 14.1184C13.1721 14.0164 13.343 13.952 13.5221 13.9252C13.7011 13.8984 13.8765 13.908 14.0482 13.954C14.267 14.0126 14.4456 14.1009 14.5838 14.2188C14.724 14.3372 14.8268 14.478 14.8922 14.6411C14.9582 14.8024 14.9912 14.9811 14.9912 15.1772C14.9918 15.3715 14.9632 15.5761 14.9056 15.7912L14.8426 16.0261L12.5561 15.4134L12.6706 14.9861L14.4336 15.4585L14.4442 15.4189C14.4731 15.281 14.4802 15.1414 14.4655 15C14.4527 14.859 14.4056 14.7332 14.324 14.6223C14.2425 14.5115 14.1121 14.4321 13.9329 14.3841C13.8141 14.3522 13.6978 14.3484 13.5842 14.3725C13.471 14.3948 13.3655 14.4454 13.2675 14.5243C13.1695 14.6032 13.0818 14.708 13.0043 14.8389C12.9267 14.9698 12.8632 15.1277 12.8137 15.3126L12.7818 15.4314C12.7429 15.5767 12.726 15.7188 12.7313 15.8577C12.7389 15.9952 12.7685 16.1224 12.8201 16.2394C12.8735 16.3568 12.9486 16.4578 13.0452 16.5423C13.1438 16.6274 13.2647 16.6891 13.4081 16.7275C13.593 16.7771 13.7597 16.7813 13.9082 16.7402C14.0567 16.6991 14.196 16.6283 14.3261 16.5276L14.5756 16.8644C14.4827 16.9467 14.3732 17.0195 14.2471 17.0828C14.121 17.146 13.9758 17.186 13.8116 17.2026C13.6493 17.2198 13.4672 17.2013 13.2653 17.1473Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_6_77" x="0.202041" y="-0.297958" width="20.0959" height="20.0959" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.5" dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_6_77"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_6_77" x="11.7372" y="4.56224" width="6.24645" height="5.96957" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_77"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_77" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_77"/>
|
||||
</filter>
|
||||
<filter id="filter2_di_6_77" x="0.673153" y="9.13222" width="15.9027" height="10.3343" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="-0.5" dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.75"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_6_77"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_6_77" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dx="0.25" dy="-0.25"/>
|
||||
<feGaussianBlur stdDeviation="0.375"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_6_77"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_6_77" x1="12.3431" y1="0.202042" x2="12.3431" y2="16.202" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E168FF"/>
|
||||
<stop offset="1" stop-color="#C848E9"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_6_77">
|
||||
<rect width="20" height="20" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_6_77">
|
||||
<rect x="4.34315" y="0.202042" width="16" height="16" rx="1" transform="rotate(15 4.34315 0.202042)" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 7.5 KiB |
29
crates/lune-roblox/Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "lune-roblox"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Roblox library for Lune"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.9.9", features = ["luau"] }
|
||||
|
||||
glam = "0.27"
|
||||
rand = "0.8"
|
||||
thiserror = "1.0"
|
||||
once_cell = "1.17"
|
||||
|
||||
rbx_binary = "1.0.0"
|
||||
rbx_dom_weak = "3.0.0"
|
||||
rbx_reflection = "5.0.0"
|
||||
rbx_reflection_database = "1.0.0"
|
||||
rbx_xml = "1.0.0"
|
||||
|
||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
73
crates/lune-roblox/src/datatypes/attributes.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
|
||||
|
||||
use super::extension::DomValueExt;
|
||||
|
||||
/**
|
||||
Checks if the given name is a valid attribute name.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the name starts with the prefix "RBX".
|
||||
- If the name contains any characters other than alphanumeric characters and underscore.
|
||||
- If the name is longer than 100 characters.
|
||||
*/
|
||||
pub fn ensure_valid_attribute_name(name: impl AsRef<str>) -> LuaResult<()> {
|
||||
let name = name.as_ref();
|
||||
if name.to_ascii_uppercase().starts_with("RBX") {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Attribute names must not start with the prefix \"RBX\"".to_string(),
|
||||
))
|
||||
} else if !name.chars().all(|c| c == '_' || c.is_alphanumeric()) {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Attribute names must only use alphanumeric characters and underscore".to_string(),
|
||||
))
|
||||
} else if name.len() > 100 {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Attribute names must be 100 characters or less in length".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if the given value is a valid attribute value.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the value is not a valid attribute type.
|
||||
*/
|
||||
pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
|
||||
let is_valid = matches!(
|
||||
value.ty(),
|
||||
DomType::Bool
|
||||
| DomType::BrickColor
|
||||
| DomType::CFrame
|
||||
| DomType::Color3
|
||||
| DomType::ColorSequence
|
||||
| DomType::EnumItem
|
||||
| DomType::Float32
|
||||
| DomType::Float64
|
||||
| DomType::Font
|
||||
| DomType::Int32
|
||||
| DomType::Int64
|
||||
| DomType::NumberRange
|
||||
| DomType::NumberSequence
|
||||
| DomType::Rect
|
||||
| DomType::String
|
||||
| DomType::UDim
|
||||
| DomType::UDim2
|
||||
| DomType::Vector2
|
||||
| DomType::Vector3
|
||||
);
|
||||
if is_valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"'{}' is not a valid attribute type",
|
||||
value.ty().variant_name().unwrap_or("???")
|
||||
)))
|
||||
}
|
||||
}
|
345
crates/lune-roblox/src/datatypes/conversion.rs
Normal file
|
@ -0,0 +1,345 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
|
||||
|
||||
use crate::{datatypes::extension::DomValueExt, instance::Instance};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub(crate) trait LuaToDomValue<'lua> {
|
||||
/**
|
||||
Converts a lua value into a weak dom value.
|
||||
|
||||
If a `variant_type` is given the conversion will be more strict
|
||||
and also more accurate, it should be given whenever possible.
|
||||
*/
|
||||
fn lua_to_dom_value(
|
||||
&self,
|
||||
lua: &'lua Lua,
|
||||
variant_type: Option<DomType>,
|
||||
) -> DomConversionResult<DomValue>;
|
||||
}
|
||||
|
||||
pub(crate) trait DomValueToLua<'lua>: Sized {
|
||||
/**
|
||||
Converts a weak dom value into a lua value.
|
||||
*/
|
||||
fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult<Self>;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Blanket trait implementations for converting between LuaValue and rbx_dom Variant values
|
||||
|
||||
These should be considered stable and done, already containing all of the known primitives
|
||||
|
||||
See bottom of module for implementations between our custom datatypes and lua userdata
|
||||
|
||||
*/
|
||||
|
||||
impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
|
||||
fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult<Self> {
|
||||
use rbx_dom_weak::types as dom;
|
||||
|
||||
match LuaAnyUserData::dom_value_to_lua(lua, variant) {
|
||||
Ok(value) => Ok(LuaValue::UserData(value)),
|
||||
Err(e) => match variant {
|
||||
DomValue::Bool(b) => Ok(LuaValue::Boolean(*b)),
|
||||
DomValue::Int64(i) => Ok(LuaValue::Number(*i as f64)),
|
||||
DomValue::Int32(i) => Ok(LuaValue::Number(*i as f64)),
|
||||
DomValue::Float64(n) => Ok(LuaValue::Number(*n)),
|
||||
DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)),
|
||||
DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
|
||||
DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)),
|
||||
DomValue::ContentId(s) => Ok(LuaValue::String(
|
||||
lua.create_string(AsRef::<str>::as_ref(s))?,
|
||||
)),
|
||||
|
||||
// NOTE: Dom references may point to instances that
|
||||
// no longer exist, so we handle that here instead of
|
||||
// in the userdata conversion to be able to return nils
|
||||
DomValue::Ref(value) => match Instance::new_opt(*value) {
|
||||
Some(inst) => Ok(inst.into_lua(lua)?),
|
||||
None => Ok(LuaValue::Nil),
|
||||
},
|
||||
|
||||
// NOTE: Some values are either optional or default and we should handle
|
||||
// that properly here since the userdata conversion above will always fail
|
||||
DomValue::OptionalCFrame(None)
|
||||
| DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => {
|
||||
Ok(LuaValue::Nil)
|
||||
}
|
||||
|
||||
_ => Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> {
|
||||
fn lua_to_dom_value(
|
||||
&self,
|
||||
lua: &'lua Lua,
|
||||
variant_type: Option<DomType>,
|
||||
) -> DomConversionResult<DomValue> {
|
||||
use rbx_dom_weak::types as dom;
|
||||
|
||||
if let Some(variant_type) = variant_type {
|
||||
match (self, variant_type) {
|
||||
(LuaValue::Boolean(b), DomType::Bool) => Ok(DomValue::Bool(*b)),
|
||||
|
||||
(LuaValue::Integer(i), DomType::Int64) => Ok(DomValue::Int64(*i as i64)),
|
||||
(LuaValue::Integer(i), DomType::Int32) => Ok(DomValue::Int32(*i)),
|
||||
(LuaValue::Integer(i), DomType::Float64) => Ok(DomValue::Float64(*i as f64)),
|
||||
(LuaValue::Integer(i), DomType::Float32) => Ok(DomValue::Float32(*i as f32)),
|
||||
|
||||
(LuaValue::Number(n), DomType::Int64) => Ok(DomValue::Int64(*n as i64)),
|
||||
(LuaValue::Number(n), DomType::Int32) => Ok(DomValue::Int32(*n as i32)),
|
||||
(LuaValue::Number(n), DomType::Float64) => Ok(DomValue::Float64(*n)),
|
||||
(LuaValue::Number(n), DomType::Float32) => Ok(DomValue::Float32(*n as f32)),
|
||||
|
||||
(LuaValue::String(s), DomType::String) => {
|
||||
Ok(DomValue::String(s.to_str()?.to_string()))
|
||||
}
|
||||
(LuaValue::String(s), DomType::BinaryString) => {
|
||||
Ok(DomValue::BinaryString(s.as_ref().into()))
|
||||
}
|
||||
(LuaValue::String(s), DomType::ContentId) => {
|
||||
Ok(DomValue::ContentId(s.to_str()?.to_string().into()))
|
||||
}
|
||||
|
||||
// NOTE: Some values are either optional or default and we
|
||||
// should handle that here before trying to convert as userdata
|
||||
(LuaValue::Nil, DomType::OptionalCFrame) => Ok(DomValue::OptionalCFrame(None)),
|
||||
(LuaValue::Nil, DomType::PhysicalProperties) => Ok(DomValue::PhysicalProperties(
|
||||
dom::PhysicalProperties::Default,
|
||||
)),
|
||||
|
||||
(LuaValue::UserData(u), d) => u.lua_to_dom_value(lua, Some(d)),
|
||||
|
||||
(v, d) => Err(DomConversionError::ToDomValue {
|
||||
to: d.variant_name().unwrap_or("???"),
|
||||
from: v.type_name(),
|
||||
detail: None,
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
match self {
|
||||
LuaValue::Boolean(b) => Ok(DomValue::Bool(*b)),
|
||||
LuaValue::Integer(i) => Ok(DomValue::Int32(*i)),
|
||||
LuaValue::Number(n) => Ok(DomValue::Float64(*n)),
|
||||
LuaValue::String(s) => Ok(DomValue::String(s.to_str()?.to_string())),
|
||||
LuaValue::UserData(u) => u.lua_to_dom_value(lua, None),
|
||||
v => Err(DomConversionError::ToDomValue {
|
||||
to: "unknown",
|
||||
from: v.type_name(),
|
||||
detail: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Trait implementations for converting between all of
|
||||
our custom datatypes and generic Lua userdata values
|
||||
|
||||
NOTE: When adding a new datatype, make sure to add it below to _both_
|
||||
of the traits and not just one to allow for bidirectional conversion
|
||||
|
||||
*/
|
||||
|
||||
macro_rules! dom_to_userdata {
|
||||
($lua:expr, $value:ident => $to_type:ty) => {
|
||||
Ok($lua.create_userdata(Into::<$to_type>::into($value.clone()))?)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
Converts a generic lua userdata to an rbx-dom type.
|
||||
|
||||
Since the type of the userdata needs to be specified
|
||||
in an explicit manner, this macro syntax was chosen:
|
||||
|
||||
```rs
|
||||
userdata_to_dom!(value_identifier as UserdataType => DomType)
|
||||
```
|
||||
*/
|
||||
macro_rules! userdata_to_dom {
|
||||
($userdata:ident as $from_type:ty => $to_type:ty) => {
|
||||
match $userdata.borrow::<$from_type>() {
|
||||
Ok(value) => Ok(From::<$to_type>::from(value.clone().into())),
|
||||
Err(error) => match error {
|
||||
LuaError::UserDataTypeMismatch => Err(DomConversionError::ToDomValue {
|
||||
to: stringify!($to_type),
|
||||
from: "userdata",
|
||||
detail: Some("Type mismatch".to_string()),
|
||||
}),
|
||||
e => Err(DomConversionError::ToDomValue {
|
||||
to: stringify!($to_type),
|
||||
from: "userdata",
|
||||
detail: Some(format!("Internal error: {e}")),
|
||||
}),
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
|
||||
#[rustfmt::skip]
|
||||
fn dom_value_to_lua(lua: &'lua Lua, variant: &DomValue) -> DomConversionResult<Self> {
|
||||
use super::types::*;
|
||||
|
||||
use rbx_dom_weak::types as dom;
|
||||
|
||||
match variant {
|
||||
DomValue::Axes(value) => dom_to_userdata!(lua, value => Axes),
|
||||
DomValue::BrickColor(value) => dom_to_userdata!(lua, value => BrickColor),
|
||||
DomValue::CFrame(value) => dom_to_userdata!(lua, value => CFrame),
|
||||
DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3),
|
||||
DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3),
|
||||
DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence),
|
||||
DomValue::Content(value) => dom_to_userdata!(lua, value => Content),
|
||||
DomValue::EnumItem(value) => dom_to_userdata!(lua, value => EnumItem),
|
||||
DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces),
|
||||
DomValue::Font(value) => dom_to_userdata!(lua, value => Font),
|
||||
DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange),
|
||||
DomValue::NumberSequence(value) => dom_to_userdata!(lua, value => NumberSequence),
|
||||
DomValue::Ray(value) => dom_to_userdata!(lua, value => Ray),
|
||||
DomValue::Rect(value) => dom_to_userdata!(lua, value => Rect),
|
||||
DomValue::Region3(value) => dom_to_userdata!(lua, value => Region3),
|
||||
DomValue::Region3int16(value) => dom_to_userdata!(lua, value => Region3int16),
|
||||
DomValue::UDim(value) => dom_to_userdata!(lua, value => UDim),
|
||||
DomValue::UDim2(value) => dom_to_userdata!(lua, value => UDim2),
|
||||
DomValue::Vector2(value) => dom_to_userdata!(lua, value => Vector2),
|
||||
DomValue::Vector2int16(value) => dom_to_userdata!(lua, value => Vector2int16),
|
||||
DomValue::Vector3(value) => dom_to_userdata!(lua, value => Vector3),
|
||||
DomValue::Vector3int16(value) => dom_to_userdata!(lua, value => Vector3int16),
|
||||
|
||||
// NOTE: The none and default variants of these types are handled in
|
||||
// DomValueToLua for the LuaValue type instead, allowing for nil/default
|
||||
DomValue::OptionalCFrame(Some(value)) => dom_to_userdata!(lua, value => CFrame),
|
||||
DomValue::PhysicalProperties(dom::PhysicalProperties::Custom(value)) => {
|
||||
dom_to_userdata!(lua, value => PhysicalProperties)
|
||||
},
|
||||
|
||||
v => {
|
||||
Err(DomConversionError::FromDomValue {
|
||||
from: v.variant_name().unwrap_or("???"),
|
||||
to: "userdata",
|
||||
detail: Some("Type not supported".to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
|
||||
#[rustfmt::skip]
|
||||
fn lua_to_dom_value(
|
||||
&self,
|
||||
_: &'lua Lua,
|
||||
variant_type: Option<DomType>,
|
||||
) -> DomConversionResult<DomValue> {
|
||||
use super::types::*;
|
||||
|
||||
use rbx_dom_weak::types as dom;
|
||||
|
||||
if let Some(variant_type) = variant_type {
|
||||
/*
|
||||
Strict target type, use it to skip checking the actual
|
||||
type of the userdata and try to just do a pure conversion
|
||||
*/
|
||||
match variant_type {
|
||||
DomType::Axes => userdata_to_dom!(self as Axes => dom::Axes),
|
||||
DomType::BrickColor => userdata_to_dom!(self as BrickColor => dom::BrickColor),
|
||||
DomType::CFrame => userdata_to_dom!(self as CFrame => dom::CFrame),
|
||||
DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3),
|
||||
DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8),
|
||||
DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence),
|
||||
DomType::Content => userdata_to_dom!(self as Content => dom::Content),
|
||||
DomType::EnumItem => userdata_to_dom!(self as EnumItem => dom::EnumItem),
|
||||
DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces),
|
||||
DomType::Font => userdata_to_dom!(self as Font => dom::Font),
|
||||
DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange),
|
||||
DomType::NumberSequence => userdata_to_dom!(self as NumberSequence => dom::NumberSequence),
|
||||
DomType::Ray => userdata_to_dom!(self as Ray => dom::Ray),
|
||||
DomType::Rect => userdata_to_dom!(self as Rect => dom::Rect),
|
||||
DomType::Ref => userdata_to_dom!(self as Instance => dom::Ref),
|
||||
DomType::Region3 => userdata_to_dom!(self as Region3 => dom::Region3),
|
||||
DomType::Region3int16 => userdata_to_dom!(self as Region3int16 => dom::Region3int16),
|
||||
DomType::UDim => userdata_to_dom!(self as UDim => dom::UDim),
|
||||
DomType::UDim2 => userdata_to_dom!(self as UDim2 => dom::UDim2),
|
||||
DomType::Vector2 => userdata_to_dom!(self as Vector2 => dom::Vector2),
|
||||
DomType::Vector2int16 => userdata_to_dom!(self as Vector2int16 => dom::Vector2int16),
|
||||
DomType::Vector3 => userdata_to_dom!(self as Vector3 => dom::Vector3),
|
||||
DomType::Vector3int16 => userdata_to_dom!(self as Vector3int16 => dom::Vector3int16),
|
||||
|
||||
// NOTE: The none and default variants of these types are handled in
|
||||
// LuaToDomValue for the LuaValue type instead, allowing for nil/default
|
||||
DomType::OptionalCFrame => {
|
||||
return match self.borrow::<CFrame>() {
|
||||
Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"),
|
||||
Ok(value) => Ok(DomValue::OptionalCFrame(Some(dom::CFrame::from(*value)))),
|
||||
}
|
||||
}
|
||||
DomType::PhysicalProperties => {
|
||||
return match self.borrow::<PhysicalProperties>() {
|
||||
Err(_) => unreachable!("Invalid use of conversion method, should be using LuaValue"),
|
||||
Ok(value) => {
|
||||
let props = dom::CustomPhysicalProperties::from(*value);
|
||||
let custom = dom::PhysicalProperties::Custom(props);
|
||||
Ok(DomValue::PhysicalProperties(custom))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ty => {
|
||||
Err(DomConversionError::ToDomValue {
|
||||
to: ty.variant_name().unwrap_or("???"),
|
||||
from: "userdata",
|
||||
detail: Some("Type not supported".to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
Non-strict target type, here we need to do manual typechecks
|
||||
on the userdata to see what we should be converting it into
|
||||
|
||||
This is used for example for attributes, where the wanted
|
||||
type is not known by the dom and instead determined by the user
|
||||
*/
|
||||
match self {
|
||||
value if value.is::<Axes>() => userdata_to_dom!(value as Axes => dom::Axes),
|
||||
value if value.is::<BrickColor>() => userdata_to_dom!(value as BrickColor => dom::BrickColor),
|
||||
value if value.is::<CFrame>() => userdata_to_dom!(value as CFrame => dom::CFrame),
|
||||
value if value.is::<Color3>() => userdata_to_dom!(value as Color3 => dom::Color3),
|
||||
value if value.is::<ColorSequence>() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence),
|
||||
value if value.is::<EnumItem>() => userdata_to_dom!(value as EnumItem => dom::EnumItem),
|
||||
value if value.is::<Faces>() => userdata_to_dom!(value as Faces => dom::Faces),
|
||||
value if value.is::<Font>() => userdata_to_dom!(value as Font => dom::Font),
|
||||
value if value.is::<Instance>() => userdata_to_dom!(value as Instance => dom::Ref),
|
||||
value if value.is::<NumberRange>() => userdata_to_dom!(value as NumberRange => dom::NumberRange),
|
||||
value if value.is::<NumberSequence>() => userdata_to_dom!(value as NumberSequence => dom::NumberSequence),
|
||||
value if value.is::<Ray>() => userdata_to_dom!(value as Ray => dom::Ray),
|
||||
value if value.is::<Rect>() => userdata_to_dom!(value as Rect => dom::Rect),
|
||||
value if value.is::<Region3>() => userdata_to_dom!(value as Region3 => dom::Region3),
|
||||
value if value.is::<Region3int16>() => userdata_to_dom!(value as Region3int16 => dom::Region3int16),
|
||||
value if value.is::<UDim>() => userdata_to_dom!(value as UDim => dom::UDim),
|
||||
value if value.is::<UDim2>() => userdata_to_dom!(value as UDim2 => dom::UDim2),
|
||||
value if value.is::<Vector2>() => userdata_to_dom!(value as Vector2 => dom::Vector2),
|
||||
value if value.is::<Vector2int16>() => userdata_to_dom!(value as Vector2int16 => dom::Vector2int16),
|
||||
value if value.is::<Vector3>() => userdata_to_dom!(value as Vector3 => dom::Vector3),
|
||||
value if value.is::<Vector3int16>() => userdata_to_dom!(value as Vector3int16 => dom::Vector3int16),
|
||||
|
||||
_ => Err(DomConversionError::ToDomValue {
|
||||
to: "unknown",
|
||||
from: "userdata",
|
||||
detail: Some("Type not supported".to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
61
crates/lune-roblox/src/datatypes/extension.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use super::*;
|
||||
|
||||
pub(crate) trait DomValueExt {
|
||||
fn variant_name(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
impl DomValueExt for DomType {
|
||||
fn variant_name(&self) -> Option<&'static str> {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use DomType::*;
|
||||
Some(match self {
|
||||
Attributes => "Attributes",
|
||||
Axes => "Axes",
|
||||
BinaryString => "BinaryString",
|
||||
Bool => "Bool",
|
||||
BrickColor => "BrickColor",
|
||||
CFrame => "CFrame",
|
||||
Color3 => "Color3",
|
||||
Color3uint8 => "Color3uint8",
|
||||
ColorSequence => "ColorSequence",
|
||||
Content => "Content",
|
||||
ContentId => "ContentId",
|
||||
Enum => "Enum",
|
||||
EnumItem => "EnumItem",
|
||||
Faces => "Faces",
|
||||
Float32 => "Float32",
|
||||
Float64 => "Float64",
|
||||
Font => "Font",
|
||||
Int32 => "Int32",
|
||||
Int64 => "Int64",
|
||||
MaterialColors => "MaterialColors",
|
||||
NumberRange => "NumberRange",
|
||||
NumberSequence => "NumberSequence",
|
||||
PhysicalProperties => "PhysicalProperties",
|
||||
Ray => "Ray",
|
||||
Rect => "Rect",
|
||||
Ref => "Ref",
|
||||
Region3 => "Region3",
|
||||
Region3int16 => "Region3int16",
|
||||
SharedString => "SharedString",
|
||||
String => "String",
|
||||
Tags => "Tags",
|
||||
UDim => "UDim",
|
||||
UDim2 => "UDim2",
|
||||
UniqueId => "UniqueId",
|
||||
Vector2 => "Vector2",
|
||||
Vector2int16 => "Vector2int16",
|
||||
Vector3 => "Vector3",
|
||||
Vector3int16 => "Vector3int16",
|
||||
OptionalCFrame => "OptionalCFrame",
|
||||
SecurityCapabilities => "SecurityCapabilities",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DomValueExt for DomValue {
|
||||
fn variant_name(&self) -> Option<&'static str> {
|
||||
self.ty().variant_name()
|
||||
}
|
||||
}
|
13
crates/lune-roblox/src/datatypes/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
pub(crate) use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
|
||||
|
||||
pub mod attributes;
|
||||
pub mod conversion;
|
||||
pub mod extension;
|
||||
pub mod result;
|
||||
pub mod types;
|
||||
|
||||
mod util;
|
||||
|
||||
use result::*;
|
||||
|
||||
pub use crate::shared::userdata::*;
|
75
crates/lune-roblox/src/datatypes/result.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use core::fmt;
|
||||
|
||||
use std::error::Error;
|
||||
use std::io::Error as IoError;
|
||||
|
||||
use mlua::Error as LuaError;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum DomConversionError {
|
||||
LuaError(LuaError),
|
||||
External {
|
||||
message: String,
|
||||
},
|
||||
FromDomValue {
|
||||
from: &'static str,
|
||||
to: &'static str,
|
||||
detail: Option<String>,
|
||||
},
|
||||
ToDomValue {
|
||||
to: &'static str,
|
||||
from: &'static str,
|
||||
detail: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for DomConversionError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::LuaError(error) => error.to_string(),
|
||||
Self::External { message } => message.to_string(),
|
||||
Self::FromDomValue { from, to, detail } | Self::ToDomValue { from, to, detail } => {
|
||||
match detail {
|
||||
Some(d) => format!("Failed to convert from '{from}' into '{to}' - {d}"),
|
||||
None => format!("Failed to convert from '{from}' into '{to}'",),
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for DomConversionError {}
|
||||
|
||||
impl From<DomConversionError> for LuaError {
|
||||
fn from(value: DomConversionError) -> Self {
|
||||
use DomConversionError as E;
|
||||
match value {
|
||||
E::LuaError(e) => e,
|
||||
E::External { message } => LuaError::external(message),
|
||||
E::FromDomValue { .. } | E::ToDomValue { .. } => {
|
||||
LuaError::RuntimeError(value.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LuaError> for DomConversionError {
|
||||
fn from(value: LuaError) -> Self {
|
||||
Self::LuaError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IoError> for DomConversionError {
|
||||
fn from(value: IoError) -> Self {
|
||||
DomConversionError::External {
|
||||
message: value.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type DomConversionResult<T> = Result<T, DomConversionError>;
|
126
crates/lune-roblox/src/datatypes/types/axes.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Axes as DomAxes;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
/**
|
||||
An implementation of the [Axes](https://create.roblox.com/docs/reference/engine/datatypes/Axes) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the Axes class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Axes {
|
||||
pub(crate) x: bool,
|
||||
pub(crate) y: bool,
|
||||
pub(crate) z: bool,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for Axes {
|
||||
const EXPORT_NAME: &'static str = "Axes";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let axes_new = |_, args: LuaMultiValue| {
|
||||
let mut x = false;
|
||||
let mut y = false;
|
||||
let mut z = false;
|
||||
|
||||
let mut check = |e: &EnumItem| {
|
||||
if e.parent.desc.name == "Axis" {
|
||||
match &e.name {
|
||||
name if name == "X" => x = true,
|
||||
name if name == "Y" => y = true,
|
||||
name if name == "Z" => z = true,
|
||||
_ => {}
|
||||
}
|
||||
} else if e.parent.desc.name == "NormalId" {
|
||||
match &e.name {
|
||||
name if name == "Left" || name == "Right" => x = true,
|
||||
name if name == "Top" || name == "Bottom" => y = true,
|
||||
name if name == "Front" || name == "Back" => z = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (index, arg) in args.into_iter().enumerate() {
|
||||
if let LuaValue::UserData(u) = arg {
|
||||
if let Ok(e) = u.borrow::<EnumItem>() {
|
||||
check(&e);
|
||||
} else {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected argument #{index} to be an EnumItem, got userdata",
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected argument #{} to be an EnumItem, got {}",
|
||||
index,
|
||||
arg.type_name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Axes { x, y, z })
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", axes_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Axes {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("X", |_, this| Ok(this.x));
|
||||
fields.add_field_method_get("Y", |_, this| Ok(this.y));
|
||||
fields.add_field_method_get("Z", |_, this| Ok(this.z));
|
||||
fields.add_field_method_get("Left", |_, this| Ok(this.x));
|
||||
fields.add_field_method_get("Right", |_, this| Ok(this.x));
|
||||
fields.add_field_method_get("Top", |_, this| Ok(this.y));
|
||||
fields.add_field_method_get("Bottom", |_, this| Ok(this.y));
|
||||
fields.add_field_method_get("Front", |_, this| Ok(this.z));
|
||||
fields.add_field_method_get("Back", |_, this| Ok(this.z));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Axes {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let write = make_list_writer();
|
||||
write(f, self.x, "X")?;
|
||||
write(f, self.y, "Y")?;
|
||||
write(f, self.z, "Z")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomAxes> for Axes {
|
||||
fn from(v: DomAxes) -> Self {
|
||||
let bits = v.bits();
|
||||
Self {
|
||||
x: (bits & 1) == 1,
|
||||
y: ((bits >> 1) & 1) == 1,
|
||||
z: ((bits >> 2) & 1) == 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Axes> for DomAxes {
|
||||
fn from(v: Axes) -> Self {
|
||||
let mut bits = 0;
|
||||
bits += v.x as u8;
|
||||
bits += (v.y as u8) << 1;
|
||||
bits += (v.z as u8) << 2;
|
||||
DomAxes::from_bits(bits).expect("Invalid bits")
|
||||
}
|
||||
}
|
441
crates/lune-roblox/src/datatypes/types/brick_color.rs
Normal file
|
@ -0,0 +1,441 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rand::seq::SliceRandom;
|
||||
use rbx_dom_weak::types::BrickColor as DomBrickColor;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Color3};
|
||||
|
||||
/**
|
||||
An implementation of the [BrickColor](https://create.roblox.com/docs/reference/engine/datatypes/BrickColor) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `BrickColor` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct BrickColor {
|
||||
// Unfortunately we can't use DomBrickColor as the backing type here
|
||||
// because it does not expose any way of getting the actual rgb colors :-(
|
||||
pub(crate) number: u16,
|
||||
pub(crate) name: &'static str,
|
||||
pub(crate) rgb: (u8, u8, u8),
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for BrickColor {
|
||||
const EXPORT_NAME: &'static str = "BrickColor";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
type ArgsNumber = u16;
|
||||
type ArgsName = String;
|
||||
type ArgsRgb = (u8, u8, u8);
|
||||
type ArgsColor3<'lua> = LuaUserDataRef<'lua, Color3>;
|
||||
|
||||
let brick_color_new = |lua, args: LuaMultiValue| {
|
||||
if let Ok(number) = ArgsNumber::from_lua_multi(args.clone(), lua) {
|
||||
Ok(color_from_number(number))
|
||||
} else if let Ok(name) = ArgsName::from_lua_multi(args.clone(), lua) {
|
||||
Ok(color_from_name(name))
|
||||
} else if let Ok((r, g, b)) = ArgsRgb::from_lua_multi(args.clone(), lua) {
|
||||
Ok(color_from_rgb(r, g, b))
|
||||
} else if let Ok(color) = ArgsColor3::from_lua_multi(args.clone(), lua) {
|
||||
Ok(Self::from(*color))
|
||||
} else {
|
||||
// FUTURE: Better error message here using given arg types
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let brick_color_palette = |_, index: u16| {
|
||||
if index == 0 {
|
||||
Err(LuaError::RuntimeError("Invalid index".to_string()))
|
||||
} else if let Some(number) = BRICK_COLOR_PALETTE.get((index - 1) as usize) {
|
||||
Ok(color_from_number(*number))
|
||||
} else {
|
||||
Err(LuaError::RuntimeError("Invalid index".to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
let brick_color_random = |_, ()| {
|
||||
let number = BRICK_COLOR_PALETTE.choose(&mut rand::thread_rng());
|
||||
Ok(color_from_number(*number.unwrap()))
|
||||
};
|
||||
|
||||
let mut builder = TableBuilder::new(lua)?
|
||||
.with_function("new", brick_color_new)?
|
||||
.with_function("palette", brick_color_palette)?
|
||||
.with_function("random", brick_color_random)?;
|
||||
|
||||
for (name, number) in BRICK_COLOR_CONSTRUCTORS {
|
||||
let f = |_, ()| Ok(color_from_number(*number));
|
||||
builder = builder.with_function(*name, f)?;
|
||||
}
|
||||
|
||||
builder.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for BrickColor {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Number", |_, this| Ok(this.number));
|
||||
fields.add_field_method_get("Name", |_, this| Ok(this.name));
|
||||
fields.add_field_method_get("R", |_, this| Ok(this.rgb.0 as f32 / 255f32));
|
||||
fields.add_field_method_get("G", |_, this| Ok(this.rgb.1 as f32 / 255f32));
|
||||
fields.add_field_method_get("B", |_, this| Ok(this.rgb.2 as f32 / 255f32));
|
||||
fields.add_field_method_get("r", |_, this| Ok(this.rgb.0 as f32 / 255f32));
|
||||
fields.add_field_method_get("g", |_, this| Ok(this.rgb.1 as f32 / 255f32));
|
||||
fields.add_field_method_get("b", |_, this| Ok(this.rgb.2 as f32 / 255f32));
|
||||
fields.add_field_method_get("Color", |_, this| Ok(Color3::from(*this)));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BrickColor {
|
||||
fn default() -> Self {
|
||||
color_from_number(BRICK_COLOR_DEFAULT)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for BrickColor {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color3> for BrickColor {
|
||||
fn from(value: Color3) -> Self {
|
||||
let r = value.r.clamp(u8::MIN as f32, u8::MAX as f32) as u8;
|
||||
let g = value.g.clamp(u8::MIN as f32, u8::MAX as f32) as u8;
|
||||
let b = value.b.clamp(u8::MIN as f32, u8::MAX as f32) as u8;
|
||||
color_from_rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrickColor> for Color3 {
|
||||
fn from(value: BrickColor) -> Self {
|
||||
Self {
|
||||
r: (value.rgb.0 as f32) / 255.0,
|
||||
g: (value.rgb.1 as f32) / 255.0,
|
||||
b: (value.rgb.2 as f32) / 255.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomBrickColor> for BrickColor {
|
||||
fn from(v: DomBrickColor) -> Self {
|
||||
color_from_name(v.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BrickColor> for DomBrickColor {
|
||||
fn from(v: BrickColor) -> Self {
|
||||
DomBrickColor::from_number(v.number).unwrap_or(DomBrickColor::MediumStoneGrey)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
NOTE: The brick color definitions below are generated using
|
||||
the brick_color script in the scripts dir next to src, which can
|
||||
be ran using `cargo run packages/lib-roblox/scripts/brick_color`
|
||||
|
||||
*/
|
||||
|
||||
type BrickColorDef = &'static (u16, &'static str, (u8, u8, u8));
|
||||
|
||||
impl From<BrickColorDef> for BrickColor {
|
||||
fn from(value: BrickColorDef) -> Self {
|
||||
Self {
|
||||
number: value.0,
|
||||
name: value.1,
|
||||
rgb: value.2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BRICK_COLOR_DEFAULT_VALUE: BrickColorDef =
|
||||
&BRICK_COLOR_VALUES[(BRICK_COLOR_DEFAULT - 1) as usize];
|
||||
|
||||
fn color_from_number(index: u16) -> BrickColor {
|
||||
BRICK_COLOR_VALUES
|
||||
.iter()
|
||||
.find(|color| color.0 == index)
|
||||
.unwrap_or(BRICK_COLOR_DEFAULT_VALUE)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn color_from_name(name: impl AsRef<str>) -> BrickColor {
|
||||
let name = name.as_ref();
|
||||
BRICK_COLOR_VALUES
|
||||
.iter()
|
||||
.find(|color| color.1 == name)
|
||||
.unwrap_or(BRICK_COLOR_DEFAULT_VALUE)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn color_from_rgb(r: u8, g: u8, b: u8) -> BrickColor {
|
||||
let r = r as i16;
|
||||
let g = g as i16;
|
||||
let b = b as i16;
|
||||
BRICK_COLOR_VALUES
|
||||
.iter()
|
||||
.fold(
|
||||
(None, u16::MAX),
|
||||
|(closest_color, closest_distance), color| {
|
||||
let cr = color.2 .0 as i16;
|
||||
let cg = color.2 .1 as i16;
|
||||
let cb = color.2 .2 as i16;
|
||||
let distance = ((r - cr) + (g - cg) + (b - cb)).unsigned_abs();
|
||||
if distance < closest_distance {
|
||||
(Some(color), distance)
|
||||
} else {
|
||||
(closest_color, closest_distance)
|
||||
}
|
||||
},
|
||||
)
|
||||
.0
|
||||
.unwrap_or(BRICK_COLOR_DEFAULT_VALUE)
|
||||
.into()
|
||||
}
|
||||
|
||||
const BRICK_COLOR_DEFAULT: u16 = 194;
|
||||
|
||||
const BRICK_COLOR_VALUES: &[(u16, &str, (u8, u8, u8))] = &[
|
||||
(1, "White", (242, 243, 243)),
|
||||
(2, "Grey", (161, 165, 162)),
|
||||
(3, "Light yellow", (249, 233, 153)),
|
||||
(5, "Brick yellow", (215, 197, 154)),
|
||||
(6, "Light green (Mint)", (194, 218, 184)),
|
||||
(9, "Light reddish violet", (232, 186, 200)),
|
||||
(11, "Pastel Blue", (128, 187, 219)),
|
||||
(12, "Light orange brown", (203, 132, 66)),
|
||||
(18, "Nougat", (204, 142, 105)),
|
||||
(21, "Bright red", (196, 40, 28)),
|
||||
(22, "Med. reddish violet", (196, 112, 160)),
|
||||
(23, "Bright blue", (13, 105, 172)),
|
||||
(24, "Bright yellow", (245, 205, 48)),
|
||||
(25, "Earth orange", (98, 71, 50)),
|
||||
(26, "Black", (27, 42, 53)),
|
||||
(27, "Dark grey", (109, 110, 108)),
|
||||
(28, "Dark green", (40, 127, 71)),
|
||||
(29, "Medium green", (161, 196, 140)),
|
||||
(36, "Lig. Yellowich orange", (243, 207, 155)),
|
||||
(37, "Bright green", (75, 151, 75)),
|
||||
(38, "Dark orange", (160, 95, 53)),
|
||||
(39, "Light bluish violet", (193, 202, 222)),
|
||||
(40, "Transparent", (236, 236, 236)),
|
||||
(41, "Tr. Red", (205, 84, 75)),
|
||||
(42, "Tr. Lg blue", (193, 223, 240)),
|
||||
(43, "Tr. Blue", (123, 182, 232)),
|
||||
(44, "Tr. Yellow", (247, 241, 141)),
|
||||
(45, "Light blue", (180, 210, 228)),
|
||||
(47, "Tr. Flu. Reddish orange", (217, 133, 108)),
|
||||
(48, "Tr. Green", (132, 182, 141)),
|
||||
(49, "Tr. Flu. Green", (248, 241, 132)),
|
||||
(50, "Phosph. White", (236, 232, 222)),
|
||||
(100, "Light red", (238, 196, 182)),
|
||||
(101, "Medium red", (218, 134, 122)),
|
||||
(102, "Medium blue", (110, 153, 202)),
|
||||
(103, "Light grey", (199, 193, 183)),
|
||||
(104, "Bright violet", (107, 50, 124)),
|
||||
(105, "Br. yellowish orange", (226, 155, 64)),
|
||||
(106, "Bright orange", (218, 133, 65)),
|
||||
(107, "Bright bluish green", (0, 143, 156)),
|
||||
(108, "Earth yellow", (104, 92, 67)),
|
||||
(110, "Bright bluish violet", (67, 84, 147)),
|
||||
(111, "Tr. Brown", (191, 183, 177)),
|
||||
(112, "Medium bluish violet", (104, 116, 172)),
|
||||
(113, "Tr. Medi. reddish violet", (229, 173, 200)),
|
||||
(115, "Med. yellowish green", (199, 210, 60)),
|
||||
(116, "Med. bluish green", (85, 165, 175)),
|
||||
(118, "Light bluish green", (183, 215, 213)),
|
||||
(119, "Br. yellowish green", (164, 189, 71)),
|
||||
(120, "Lig. yellowish green", (217, 228, 167)),
|
||||
(121, "Med. yellowish orange", (231, 172, 88)),
|
||||
(123, "Br. reddish orange", (211, 111, 76)),
|
||||
(124, "Bright reddish violet", (146, 57, 120)),
|
||||
(125, "Light orange", (234, 184, 146)),
|
||||
(126, "Tr. Bright bluish violet", (165, 165, 203)),
|
||||
(127, "Gold", (220, 188, 129)),
|
||||
(128, "Dark nougat", (174, 122, 89)),
|
||||
(131, "Silver", (156, 163, 168)),
|
||||
(133, "Neon orange", (213, 115, 61)),
|
||||
(134, "Neon green", (216, 221, 86)),
|
||||
(135, "Sand blue", (116, 134, 157)),
|
||||
(136, "Sand violet", (135, 124, 144)),
|
||||
(137, "Medium orange", (224, 152, 100)),
|
||||
(138, "Sand yellow", (149, 138, 115)),
|
||||
(140, "Earth blue", (32, 58, 86)),
|
||||
(141, "Earth green", (39, 70, 45)),
|
||||
(143, "Tr. Flu. Blue", (207, 226, 247)),
|
||||
(145, "Sand blue metallic", (121, 136, 161)),
|
||||
(146, "Sand violet metallic", (149, 142, 163)),
|
||||
(147, "Sand yellow metallic", (147, 135, 103)),
|
||||
(148, "Dark grey metallic", (87, 88, 87)),
|
||||
(149, "Black metallic", (22, 29, 50)),
|
||||
(150, "Light grey metallic", (171, 173, 172)),
|
||||
(151, "Sand green", (120, 144, 130)),
|
||||
(153, "Sand red", (149, 121, 119)),
|
||||
(154, "Dark red", (123, 46, 47)),
|
||||
(157, "Tr. Flu. Yellow", (255, 246, 123)),
|
||||
(158, "Tr. Flu. Red", (225, 164, 194)),
|
||||
(168, "Gun metallic", (117, 108, 98)),
|
||||
(176, "Red flip/flop", (151, 105, 91)),
|
||||
(178, "Yellow flip/flop", (180, 132, 85)),
|
||||
(179, "Silver flip/flop", (137, 135, 136)),
|
||||
(180, "Curry", (215, 169, 75)),
|
||||
(190, "Fire Yellow", (249, 214, 46)),
|
||||
(191, "Flame yellowish orange", (232, 171, 45)),
|
||||
(192, "Reddish brown", (105, 64, 40)),
|
||||
(193, "Flame reddish orange", (207, 96, 36)),
|
||||
(194, "Medium stone grey", (163, 162, 165)),
|
||||
(195, "Royal blue", (70, 103, 164)),
|
||||
(196, "Dark Royal blue", (35, 71, 139)),
|
||||
(198, "Bright reddish lilac", (142, 66, 133)),
|
||||
(199, "Dark stone grey", (99, 95, 98)),
|
||||
(200, "Lemon metalic", (130, 138, 93)),
|
||||
(208, "Light stone grey", (229, 228, 223)),
|
||||
(209, "Dark Curry", (176, 142, 68)),
|
||||
(210, "Faded green", (112, 149, 120)),
|
||||
(211, "Turquoise", (121, 181, 181)),
|
||||
(212, "Light Royal blue", (159, 195, 233)),
|
||||
(213, "Medium Royal blue", (108, 129, 183)),
|
||||
(216, "Rust", (144, 76, 42)),
|
||||
(217, "Brown", (124, 92, 70)),
|
||||
(218, "Reddish lilac", (150, 112, 159)),
|
||||
(219, "Lilac", (107, 98, 155)),
|
||||
(220, "Light lilac", (167, 169, 206)),
|
||||
(221, "Bright purple", (205, 98, 152)),
|
||||
(222, "Light purple", (228, 173, 200)),
|
||||
(223, "Light pink", (220, 144, 149)),
|
||||
(224, "Light brick yellow", (240, 213, 160)),
|
||||
(225, "Warm yellowish orange", (235, 184, 127)),
|
||||
(226, "Cool yellow", (253, 234, 141)),
|
||||
(232, "Dove blue", (125, 187, 221)),
|
||||
(268, "Medium lilac", (52, 43, 117)),
|
||||
(301, "Slime green", (80, 109, 84)),
|
||||
(302, "Smoky grey", (91, 93, 105)),
|
||||
(303, "Dark blue", (0, 16, 176)),
|
||||
(304, "Parsley green", (44, 101, 29)),
|
||||
(305, "Steel blue", (82, 124, 174)),
|
||||
(306, "Storm blue", (51, 88, 130)),
|
||||
(307, "Lapis", (16, 42, 220)),
|
||||
(308, "Dark indigo", (61, 21, 133)),
|
||||
(309, "Sea green", (52, 142, 64)),
|
||||
(310, "Shamrock", (91, 154, 76)),
|
||||
(311, "Fossil", (159, 161, 172)),
|
||||
(312, "Mulberry", (89, 34, 89)),
|
||||
(313, "Forest green", (31, 128, 29)),
|
||||
(314, "Cadet blue", (159, 173, 192)),
|
||||
(315, "Electric blue", (9, 137, 207)),
|
||||
(316, "Eggplant", (123, 0, 123)),
|
||||
(317, "Moss", (124, 156, 107)),
|
||||
(318, "Artichoke", (138, 171, 133)),
|
||||
(319, "Sage green", (185, 196, 177)),
|
||||
(320, "Ghost grey", (202, 203, 209)),
|
||||
(321, "Lilac", (167, 94, 155)),
|
||||
(322, "Plum", (123, 47, 123)),
|
||||
(323, "Olivine", (148, 190, 129)),
|
||||
(324, "Laurel green", (168, 189, 153)),
|
||||
(325, "Quill grey", (223, 223, 222)),
|
||||
(327, "Crimson", (151, 0, 0)),
|
||||
(328, "Mint", (177, 229, 166)),
|
||||
(329, "Baby blue", (152, 194, 219)),
|
||||
(330, "Carnation pink", (255, 152, 220)),
|
||||
(331, "Persimmon", (255, 89, 89)),
|
||||
(332, "Maroon", (117, 0, 0)),
|
||||
(333, "Gold", (239, 184, 56)),
|
||||
(334, "Daisy orange", (248, 217, 109)),
|
||||
(335, "Pearl", (231, 231, 236)),
|
||||
(336, "Fog", (199, 212, 228)),
|
||||
(337, "Salmon", (255, 148, 148)),
|
||||
(338, "Terra Cotta", (190, 104, 98)),
|
||||
(339, "Cocoa", (86, 36, 36)),
|
||||
(340, "Wheat", (241, 231, 199)),
|
||||
(341, "Buttermilk", (254, 243, 187)),
|
||||
(342, "Mauve", (224, 178, 208)),
|
||||
(343, "Sunrise", (212, 144, 189)),
|
||||
(344, "Tawny", (150, 85, 85)),
|
||||
(345, "Rust", (143, 76, 42)),
|
||||
(346, "Cashmere", (211, 190, 150)),
|
||||
(347, "Khaki", (226, 220, 188)),
|
||||
(348, "Lily white", (237, 234, 234)),
|
||||
(349, "Seashell", (233, 218, 218)),
|
||||
(350, "Burgundy", (136, 62, 62)),
|
||||
(351, "Cork", (188, 155, 93)),
|
||||
(352, "Burlap", (199, 172, 120)),
|
||||
(353, "Beige", (202, 191, 163)),
|
||||
(354, "Oyster", (187, 179, 178)),
|
||||
(355, "Pine Cone", (108, 88, 75)),
|
||||
(356, "Fawn brown", (160, 132, 79)),
|
||||
(357, "Hurricane grey", (149, 137, 136)),
|
||||
(358, "Cloudy grey", (171, 168, 158)),
|
||||
(359, "Linen", (175, 148, 131)),
|
||||
(360, "Copper", (150, 103, 102)),
|
||||
(361, "Dirt brown", (86, 66, 54)),
|
||||
(362, "Bronze", (126, 104, 63)),
|
||||
(363, "Flint", (105, 102, 92)),
|
||||
(364, "Dark taupe", (90, 76, 66)),
|
||||
(365, "Burnt Sienna", (106, 57, 9)),
|
||||
(1001, "Institutional white", (248, 248, 248)),
|
||||
(1002, "Mid gray", (205, 205, 205)),
|
||||
(1003, "Really black", (17, 17, 17)),
|
||||
(1004, "Really red", (255, 0, 0)),
|
||||
(1005, "Deep orange", (255, 176, 0)),
|
||||
(1006, "Alder", (180, 128, 255)),
|
||||
(1007, "Dusty Rose", (163, 75, 75)),
|
||||
(1008, "Olive", (193, 190, 66)),
|
||||
(1009, "New Yeller", (255, 255, 0)),
|
||||
(1010, "Really blue", (0, 0, 255)),
|
||||
(1011, "Navy blue", (0, 32, 96)),
|
||||
(1012, "Deep blue", (33, 84, 185)),
|
||||
(1013, "Cyan", (4, 175, 236)),
|
||||
(1014, "CGA brown", (170, 85, 0)),
|
||||
(1015, "Magenta", (170, 0, 170)),
|
||||
(1016, "Pink", (255, 102, 204)),
|
||||
(1017, "Deep orange", (255, 175, 0)),
|
||||
(1018, "Teal", (18, 238, 212)),
|
||||
(1019, "Toothpaste", (0, 255, 255)),
|
||||
(1020, "Lime green", (0, 255, 0)),
|
||||
(1021, "Camo", (58, 125, 21)),
|
||||
(1022, "Grime", (127, 142, 100)),
|
||||
(1023, "Lavender", (140, 91, 159)),
|
||||
(1024, "Pastel light blue", (175, 221, 255)),
|
||||
(1025, "Pastel orange", (255, 201, 201)),
|
||||
(1026, "Pastel violet", (177, 167, 255)),
|
||||
(1027, "Pastel blue-green", (159, 243, 233)),
|
||||
(1028, "Pastel green", (204, 255, 204)),
|
||||
(1029, "Pastel yellow", (255, 255, 204)),
|
||||
(1030, "Pastel brown", (255, 204, 153)),
|
||||
(1031, "Royal purple", (98, 37, 209)),
|
||||
(1032, "Hot pink", (255, 0, 191)),
|
||||
];
|
||||
|
||||
const BRICK_COLOR_PALETTE: &[u16] = &[
|
||||
141, 301, 107, 26, 1012, 303, 1011, 304, 28, 1018, 302, 305, 306, 307, 308, 1021, 309, 310,
|
||||
1019, 135, 102, 23, 1010, 312, 313, 37, 1022, 1020, 1027, 311, 315, 1023, 1031, 316, 151, 317,
|
||||
318, 319, 1024, 314, 1013, 1006, 321, 322, 104, 1008, 119, 323, 324, 325, 320, 11, 1026, 1016,
|
||||
1032, 1015, 327, 1005, 1009, 29, 328, 1028, 208, 45, 329, 330, 331, 1004, 21, 332, 333, 24,
|
||||
334, 226, 1029, 335, 336, 342, 343, 338, 1007, 339, 133, 106, 340, 341, 1001, 1, 9, 1025, 337,
|
||||
344, 345, 1014, 105, 346, 347, 348, 349, 1030, 125, 101, 350, 192, 351, 352, 353, 354, 1002, 5,
|
||||
18, 217, 355, 356, 153, 357, 358, 359, 360, 38, 361, 362, 199, 194, 363, 364, 365, 1003,
|
||||
];
|
||||
|
||||
const BRICK_COLOR_CONSTRUCTORS: &[(&str, u16)] = &[
|
||||
("Yellow", 24),
|
||||
("White", 1),
|
||||
("Black", 26),
|
||||
("Green", 28),
|
||||
("Red", 21),
|
||||
("DarkGray", 199),
|
||||
("Blue", 23),
|
||||
("Gray", 194),
|
||||
];
|
493
crates/lune-roblox/src/datatypes/types/cframe.rs
Normal file
|
@ -0,0 +1,493 @@
|
|||
#![allow(clippy::items_after_statements)]
|
||||
|
||||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
use glam::{EulerRot, Mat3, Mat4, Quat, Vec3};
|
||||
use mlua::{prelude::*, Variadic};
|
||||
use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Vector3};
|
||||
|
||||
/**
|
||||
An implementation of the [CFrame](https://create.roblox.com/docs/reference/engine/datatypes/CFrame)
|
||||
Roblox datatype, backed by [`glam::Mat4`].
|
||||
|
||||
This implements all documented properties, methods &
|
||||
constructors of the `CFrame` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CFrame(pub Mat4);
|
||||
|
||||
impl CFrame {
|
||||
pub const IDENTITY: Self = Self(Mat4::IDENTITY);
|
||||
|
||||
fn position(&self) -> Vec3 {
|
||||
self.0.w_axis.truncate()
|
||||
}
|
||||
|
||||
fn orientation(&self) -> Mat3 {
|
||||
Mat3::from_cols(
|
||||
self.0.x_axis.truncate(),
|
||||
self.0.y_axis.truncate(),
|
||||
self.0.z_axis.truncate(),
|
||||
)
|
||||
}
|
||||
|
||||
fn inverse(&self) -> Self {
|
||||
Self(self.0.inverse())
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for CFrame {
|
||||
const EXPORT_NAME: &'static str = "CFrame";
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let cframe_angles = |_, (rx, ry, rz): (f32, f32, f32)| {
|
||||
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
|
||||
};
|
||||
|
||||
let cframe_from_axis_angle =
|
||||
|_, (v, r): (LuaUserDataRef<Vector3>, f32)| Ok(CFrame(Mat4::from_axis_angle(v.0, r)));
|
||||
|
||||
let cframe_from_euler_angles_xyz = |_, (rx, ry, rz): (f32, f32, f32)| {
|
||||
Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
|
||||
};
|
||||
|
||||
let cframe_from_euler_angles_yxz = |_, (rx, ry, rz): (f32, f32, f32)| {
|
||||
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
|
||||
};
|
||||
|
||||
let cframe_from_matrix = |_,
|
||||
(pos, rx, ry, rz): (
|
||||
LuaUserDataRef<Vector3>,
|
||||
LuaUserDataRef<Vector3>,
|
||||
LuaUserDataRef<Vector3>,
|
||||
Option<LuaUserDataRef<Vector3>>,
|
||||
)| {
|
||||
Ok(CFrame(Mat4::from_cols(
|
||||
rx.0.extend(0.0),
|
||||
ry.0.extend(0.0),
|
||||
rz.map_or_else(|| rx.0.cross(ry.0).normalize(), |r| r.0)
|
||||
.extend(0.0),
|
||||
pos.0.extend(1.0),
|
||||
)))
|
||||
};
|
||||
|
||||
let cframe_from_orientation = |_, (rx, ry, rz): (f32, f32, f32)| {
|
||||
Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz)))
|
||||
};
|
||||
|
||||
let cframe_look_at = |_,
|
||||
(from, to, up): (
|
||||
LuaUserDataRef<Vector3>,
|
||||
LuaUserDataRef<Vector3>,
|
||||
Option<LuaUserDataRef<Vector3>>,
|
||||
)| {
|
||||
Ok(CFrame(look_at(
|
||||
from.0,
|
||||
to.0,
|
||||
up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0,
|
||||
)))
|
||||
};
|
||||
|
||||
// Dynamic args constructor
|
||||
type ArgsPos<'lua> = LuaUserDataRef<'lua, Vector3>;
|
||||
type ArgsLook<'lua> = (
|
||||
LuaUserDataRef<'lua, Vector3>,
|
||||
LuaUserDataRef<'lua, Vector3>,
|
||||
Option<LuaUserDataRef<'lua, Vector3>>,
|
||||
);
|
||||
|
||||
type ArgsPosXYZ = (f32, f32, f32);
|
||||
type ArgsPosXYZQuat = (f32, f32, f32, f32, f32, f32, f32);
|
||||
type ArgsMatrix = (f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32);
|
||||
|
||||
let cframe_new = |lua, args: LuaMultiValue| match args.len() {
|
||||
0 => Ok(CFrame(Mat4::IDENTITY)),
|
||||
|
||||
1 => match ArgsPos::from_lua_multi(args, lua) {
|
||||
Ok(pos) => Ok(CFrame(Mat4::from_translation(pos.0))),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
3 => {
|
||||
if let Ok((from, to, up)) = ArgsLook::from_lua_multi(args.clone(), lua) {
|
||||
Ok(CFrame(look_at(
|
||||
from.0,
|
||||
to.0,
|
||||
up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0,
|
||||
)))
|
||||
} else if let Ok((x, y, z)) = ArgsPosXYZ::from_lua_multi(args, lua) {
|
||||
Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z))))
|
||||
} else {
|
||||
// TODO: Make this error message better
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
7 => match ArgsPosXYZQuat::from_lua_multi(args, lua) {
|
||||
Ok((x, y, z, qx, qy, qz, qw)) => Ok(CFrame(Mat4::from_rotation_translation(
|
||||
Quat::from_array([qx, qy, qz, qw]),
|
||||
Vec3::new(x, y, z),
|
||||
))),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
12 => match ArgsMatrix::from_lua_multi(args, lua) {
|
||||
Ok((x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22)) => {
|
||||
Ok(CFrame(Mat4::from_cols_array_2d(&[
|
||||
[r00, r10, r20, 0.0],
|
||||
[r01, r11, r21, 0.0],
|
||||
[r02, r12, r22, 0.0],
|
||||
[x, y, z, 1.0],
|
||||
])))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
_ => Err(LuaError::RuntimeError(format!(
|
||||
"Invalid number of arguments: expected 0, 1, 3, 7, or 12, got {}",
|
||||
args.len()
|
||||
))),
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("Angles", cframe_angles)?
|
||||
.with_value("identity", CFrame(Mat4::IDENTITY))?
|
||||
.with_function("fromAxisAngle", cframe_from_axis_angle)?
|
||||
.with_function("fromEulerAnglesXYZ", cframe_from_euler_angles_xyz)?
|
||||
.with_function("fromEulerAnglesYXZ", cframe_from_euler_angles_yxz)?
|
||||
.with_function("fromMatrix", cframe_from_matrix)?
|
||||
.with_function("fromOrientation", cframe_from_orientation)?
|
||||
.with_function("lookAt", cframe_look_at)?
|
||||
.with_function("new", cframe_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for CFrame {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Position", |_, this| Ok(Vector3(this.position())));
|
||||
fields.add_field_method_get("Rotation", |_, this| {
|
||||
Ok(CFrame(Mat4::from_cols(
|
||||
this.0.x_axis,
|
||||
this.0.y_axis,
|
||||
this.0.z_axis,
|
||||
Vec3::ZERO.extend(1.0),
|
||||
)))
|
||||
});
|
||||
fields.add_field_method_get("X", |_, this| Ok(this.position().x));
|
||||
fields.add_field_method_get("Y", |_, this| Ok(this.position().y));
|
||||
fields.add_field_method_get("Z", |_, this| Ok(this.position().z));
|
||||
fields.add_field_method_get("XVector", |_, this| Ok(Vector3(this.orientation().x_axis)));
|
||||
fields.add_field_method_get("YVector", |_, this| Ok(Vector3(this.orientation().y_axis)));
|
||||
fields.add_field_method_get("ZVector", |_, this| Ok(Vector3(this.orientation().z_axis)));
|
||||
fields.add_field_method_get("RightVector", |_, this| {
|
||||
Ok(Vector3(this.orientation().x_axis))
|
||||
});
|
||||
fields.add_field_method_get("UpVector", |_, this| Ok(Vector3(this.orientation().y_axis)));
|
||||
fields.add_field_method_get("LookVector", |_, this| {
|
||||
Ok(Vector3(-this.orientation().z_axis))
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("Inverse", |_, this, ()| Ok(this.inverse()));
|
||||
methods.add_method(
|
||||
"Lerp",
|
||||
|_, this, (goal, alpha): (LuaUserDataRef<CFrame>, f32)| {
|
||||
let quat_this = Quat::from_mat4(&this.0);
|
||||
let quat_goal = Quat::from_mat4(&goal.0);
|
||||
let translation = this
|
||||
.0
|
||||
.w_axis
|
||||
.truncate()
|
||||
.lerp(goal.0.w_axis.truncate(), alpha);
|
||||
let rotation = quat_this.slerp(quat_goal, alpha);
|
||||
Ok(CFrame(Mat4::from_rotation_translation(
|
||||
rotation,
|
||||
translation,
|
||||
)))
|
||||
},
|
||||
);
|
||||
methods.add_method("Orthonormalize", |_, this, ()| {
|
||||
let rotation = Quat::from_mat4(&this.0);
|
||||
let translation = this.0.w_axis.truncate();
|
||||
Ok(CFrame(Mat4::from_rotation_translation(
|
||||
rotation.normalize(),
|
||||
translation,
|
||||
)))
|
||||
});
|
||||
methods.add_method(
|
||||
"ToWorldSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|cf| *this * *cf)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"ToObjectSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
|
||||
let inverse = this.inverse();
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|cf| inverse * *cf)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"PointToWorldSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| *this * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"PointToObjectSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
let inverse = this.inverse();
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| inverse * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"VectorToWorldSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
let result = *this - Vector3(this.position());
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| result * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"VectorToObjectSpace",
|
||||
|_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
|
||||
let inverse = this.inverse();
|
||||
let result = inverse - Vector3(inverse.position());
|
||||
Ok(rhs
|
||||
.into_iter()
|
||||
.map(|v3| result * *v3)
|
||||
.collect::<Variadic<_>>())
|
||||
},
|
||||
);
|
||||
#[rustfmt::skip]
|
||||
methods.add_method("GetComponents", |_, this, ()| {
|
||||
let pos = this.position();
|
||||
let transposed = this.orientation().transpose();
|
||||
Ok((
|
||||
pos.x, pos.y, pos.z,
|
||||
transposed.x_axis.x, transposed.x_axis.y, transposed.x_axis.z,
|
||||
transposed.y_axis.x, transposed.y_axis.y, transposed.y_axis.z,
|
||||
transposed.z_axis.x, transposed.z_axis.y, transposed.z_axis.z,
|
||||
))
|
||||
});
|
||||
methods.add_method("ToEulerAnglesXYZ", |_, this, ()| {
|
||||
Ok(Quat::from_mat4(&this.0).to_euler(EulerRot::XYZ))
|
||||
});
|
||||
methods.add_method("ToEulerAnglesYXZ", |_, this, ()| {
|
||||
let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ);
|
||||
Ok((rx, ry, rz))
|
||||
});
|
||||
methods.add_method("ToOrientation", |_, this, ()| {
|
||||
let (ry, rx, rz) = Quat::from_mat4(&this.0).to_euler(EulerRot::YXZ);
|
||||
Ok((rx, ry, rz))
|
||||
});
|
||||
methods.add_method("ToAxisAngle", |_, this, ()| {
|
||||
let (axis, angle) = Quat::from_mat4(&this.0).to_axis_angle();
|
||||
Ok((Vector3(axis), angle))
|
||||
});
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Mul, |lua, this, rhs: LuaValue| {
|
||||
if let LuaValue::UserData(ud) = &rhs {
|
||||
if let Ok(cf) = ud.borrow::<CFrame>() {
|
||||
return lua.create_userdata(*this * *cf);
|
||||
} else if let Ok(vec) = ud.borrow::<Vector3>() {
|
||||
return lua.create_userdata(*this * *vec);
|
||||
}
|
||||
};
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: rhs.type_name(),
|
||||
to: "userdata",
|
||||
message: Some(format!(
|
||||
"Expected CFrame or Vector3, got {}",
|
||||
rhs.type_name()
|
||||
)),
|
||||
})
|
||||
});
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Add,
|
||||
|_, this, vec: LuaUserDataRef<Vector3>| Ok(*this + *vec),
|
||||
);
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Sub,
|
||||
|_, this, vec: LuaUserDataRef<Vector3>| Ok(*this - *vec),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CFrame {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let pos = self.position();
|
||||
let transposed = self.orientation().transpose();
|
||||
write!(
|
||||
f,
|
||||
"{}, {}, {}, {}",
|
||||
Vector3(pos),
|
||||
Vector3(transposed.x_axis),
|
||||
Vector3(transposed.y_axis),
|
||||
Vector3(transposed.z_axis)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for CFrame {
|
||||
type Output = Self;
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
CFrame(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<Vector3> for CFrame {
|
||||
type Output = Vector3;
|
||||
fn mul(self, rhs: Vector3) -> Self::Output {
|
||||
Vector3(self.0.project_point3(rhs.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<Vector3> for CFrame {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Vector3) -> Self::Output {
|
||||
CFrame(Mat4::from_cols(
|
||||
self.0.x_axis,
|
||||
self.0.y_axis,
|
||||
self.0.z_axis,
|
||||
self.0.w_axis + rhs.0.extend(0.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub<Vector3> for CFrame {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Vector3) -> Self::Output {
|
||||
CFrame(Mat4::from_cols(
|
||||
self.0.x_axis,
|
||||
self.0.y_axis,
|
||||
self.0.z_axis,
|
||||
self.0.w_axis - rhs.0.extend(0.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomCFrame> for CFrame {
|
||||
fn from(v: DomCFrame) -> Self {
|
||||
let transposed = v.orientation.transpose();
|
||||
CFrame(Mat4::from_cols(
|
||||
Vector3::from(transposed.x).0.extend(0.0),
|
||||
Vector3::from(transposed.y).0.extend(0.0),
|
||||
Vector3::from(transposed.z).0.extend(0.0),
|
||||
Vector3::from(v.position).0.extend(1.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CFrame> for DomCFrame {
|
||||
fn from(v: CFrame) -> Self {
|
||||
let transposed = v.orientation().transpose();
|
||||
DomCFrame {
|
||||
position: DomVector3::from(Vector3(v.position())),
|
||||
orientation: DomMatrix3::new(
|
||||
DomVector3::from(Vector3(transposed.x_axis)),
|
||||
DomVector3::from(Vector3(transposed.y_axis)),
|
||||
DomVector3::from(Vector3(transposed.z_axis)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a matrix at the position `from`, looking towards `to`.
|
||||
|
||||
[`glam`] does provide functions such as [`look_at_lh`], [`look_at_rh`] and more but
|
||||
they all create view matrices for camera transforms which is not what we want here.
|
||||
*/
|
||||
fn look_at(from: Vec3, to: Vec3, up: Vec3) -> Mat4 {
|
||||
let dir = (to - from).normalize();
|
||||
let xaxis = up.cross(dir).normalize();
|
||||
let yaxis = dir.cross(xaxis).normalize();
|
||||
|
||||
Mat4::from_cols(
|
||||
Vec3::new(xaxis.x, yaxis.x, dir.x).extend(0.0),
|
||||
Vec3::new(xaxis.y, yaxis.y, dir.y).extend(0.0),
|
||||
Vec3::new(xaxis.z, yaxis.z, dir.z).extend(0.0),
|
||||
from.extend(1.0),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod cframe_test {
|
||||
use glam::{Mat4, Vec3};
|
||||
use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3};
|
||||
|
||||
use super::CFrame;
|
||||
|
||||
#[test]
|
||||
fn dom_cframe_from_cframe() {
|
||||
let dom_cframe = DomCFrame::new(
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
DomMatrix3::new(
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
),
|
||||
);
|
||||
|
||||
let cframe = CFrame(Mat4::from_cols(
|
||||
Vec3::new(1.0, 1.0, 1.0).extend(0.0),
|
||||
Vec3::new(2.0, 2.0, 2.0).extend(0.0),
|
||||
Vec3::new(3.0, 3.0, 3.0).extend(0.0),
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(1.0),
|
||||
));
|
||||
|
||||
assert_eq!(CFrame::from(dom_cframe), cframe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cframe_from_dom_cframe() {
|
||||
let cframe = CFrame(Mat4::from_cols(
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(0.0),
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(0.0),
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(0.0),
|
||||
Vec3::new(1.0, 2.0, 3.0).extend(1.0),
|
||||
));
|
||||
|
||||
let dom_cframe = DomCFrame::new(
|
||||
DomVector3::new(1.0, 2.0, 3.0),
|
||||
DomMatrix3::new(
|
||||
DomVector3::new(1.0, 1.0, 1.0),
|
||||
DomVector3::new(2.0, 2.0, 2.0),
|
||||
DomVector3::new(3.0, 3.0, 3.0),
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(DomCFrame::from(cframe), dom_cframe);
|
||||
}
|
||||
}
|
316
crates/lune-roblox/src/datatypes/types/color3.rs
Normal file
|
@ -0,0 +1,316 @@
|
|||
#![allow(clippy::many_single_char_names)]
|
||||
|
||||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
use glam::Vec3;
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{Color3 as DomColor3, Color3uint8 as DomColor3uint8};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [Color3](https://create.roblox.com/docs/reference/engine/datatypes/Color3) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the Color3 class as of March 2023.
|
||||
|
||||
It also implements math operations for addition, subtraction, multiplication, and division,
|
||||
all of which are suspiciously missing from the Roblox implementation of the Color3 datatype.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Color3 {
|
||||
pub(crate) r: f32,
|
||||
pub(crate) g: f32,
|
||||
pub(crate) b: f32,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for Color3 {
|
||||
const EXPORT_NAME: &'static str = "Color3";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let color3_from_rgb = |_, (r, g, b): (Option<u8>, Option<u8>, Option<u8>)| {
|
||||
Ok(Color3 {
|
||||
r: (r.unwrap_or_default() as f32) / 255f32,
|
||||
g: (g.unwrap_or_default() as f32) / 255f32,
|
||||
b: (b.unwrap_or_default() as f32) / 255f32,
|
||||
})
|
||||
};
|
||||
|
||||
let color3_from_hsv = |_, (h, s, v): (f32, f32, f32)| {
|
||||
// https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
|
||||
let i = (h * 6.0).floor();
|
||||
let f = h * 6.0 - i;
|
||||
let p = v * (1.0 - s);
|
||||
let q = v * (1.0 - f * s);
|
||||
let t = v * (1.0 - (1.0 - f) * s);
|
||||
|
||||
let (r, g, b) = match (i % 6.0) as u8 {
|
||||
0 => (v, t, p),
|
||||
1 => (q, v, p),
|
||||
2 => (p, v, t),
|
||||
3 => (p, q, v),
|
||||
4 => (t, p, v),
|
||||
5 => (v, p, q),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Ok(Color3 { r, g, b })
|
||||
};
|
||||
|
||||
let color3_from_hex = |_, hex: String| {
|
||||
let trimmed = hex.trim_start_matches('#').to_ascii_uppercase();
|
||||
let chars = if trimmed.len() == 3 {
|
||||
(
|
||||
u8::from_str_radix(&trimmed[..1].repeat(2), 16),
|
||||
u8::from_str_radix(&trimmed[1..2].repeat(2), 16),
|
||||
u8::from_str_radix(&trimmed[2..3].repeat(2), 16),
|
||||
)
|
||||
} else if trimmed.len() == 6 {
|
||||
(
|
||||
u8::from_str_radix(&trimmed[..2], 16),
|
||||
u8::from_str_radix(&trimmed[2..4], 16),
|
||||
u8::from_str_radix(&trimmed[4..6], 16),
|
||||
)
|
||||
} else {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Hex color string must be 3 or 6 characters long, got {} character{}",
|
||||
trimmed.len(),
|
||||
if trimmed.len() == 1 { "" } else { "s" }
|
||||
)));
|
||||
};
|
||||
match chars {
|
||||
(Ok(r), Ok(g), Ok(b)) => Ok(Color3 {
|
||||
r: (r as f32) / 255f32,
|
||||
g: (g as f32) / 255f32,
|
||||
b: (b as f32) / 255f32,
|
||||
}),
|
||||
_ => Err(LuaError::RuntimeError(format!(
|
||||
"Hex color string '{trimmed}' contains invalid character",
|
||||
))),
|
||||
}
|
||||
};
|
||||
|
||||
let color3_new = |_, (r, g, b): (Option<f32>, Option<f32>, Option<f32>)| {
|
||||
Ok(Color3 {
|
||||
r: r.unwrap_or_default(),
|
||||
g: g.unwrap_or_default(),
|
||||
b: b.unwrap_or_default(),
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("fromRGB", color3_from_rgb)?
|
||||
.with_function("fromHSV", color3_from_hsv)?
|
||||
.with_function("fromHex", color3_from_hex)?
|
||||
.with_function("new", color3_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for Color3 {
|
||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
||||
if let LuaValue::UserData(ud) = value {
|
||||
Ok(*ud.borrow::<Color3>()?)
|
||||
} else {
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "Color3",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Color3 {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("R", |_, this| Ok(this.r));
|
||||
fields.add_field_method_get("G", |_, this| Ok(this.g));
|
||||
fields.add_field_method_get("B", |_, this| Ok(this.b));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method(
|
||||
"Lerp",
|
||||
|_, this, (rhs, alpha): (LuaUserDataRef<Color3>, f32)| {
|
||||
let v3_this = Vec3::new(this.r, this.g, this.b);
|
||||
let v3_rhs = Vec3::new(rhs.r, rhs.g, rhs.b);
|
||||
let v3 = v3_this.lerp(v3_rhs, alpha);
|
||||
Ok(Color3 {
|
||||
r: v3.x,
|
||||
g: v3.y,
|
||||
b: v3.z,
|
||||
})
|
||||
},
|
||||
);
|
||||
methods.add_method("ToHSV", |_, this, ()| {
|
||||
// https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
|
||||
let (r, g, b) = (this.r, this.g, this.b);
|
||||
let min = r.min(g).min(b);
|
||||
let max = r.max(g).max(b);
|
||||
let diff = max - min;
|
||||
|
||||
#[allow(clippy::float_cmp)]
|
||||
let hue = (match max {
|
||||
max if max == min => 0.0,
|
||||
max if max == r => (g - b) / diff + (if g < b { 6.0 } else { 0.0 }),
|
||||
max if max == g => (b - r) / diff + 2.0,
|
||||
max if max == b => (r - g) / diff + 4.0,
|
||||
_ => unreachable!(),
|
||||
}) / 6.0;
|
||||
|
||||
let sat = if max == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
(diff / max).clamp(0.0, 1.0)
|
||||
};
|
||||
|
||||
Ok((hue, sat, max))
|
||||
});
|
||||
methods.add_method("ToHex", |_, this, ()| {
|
||||
Ok(format!(
|
||||
"{:02X}{:02X}{:02X}",
|
||||
(this.r * 255.0).clamp(u8::MIN as f32, u8::MAX as f32) as u8,
|
||||
(this.g * 255.0).clamp(u8::MIN as f32, u8::MAX as f32) as u8,
|
||||
(this.b * 255.0).clamp(u8::MIN as f32, u8::MAX as f32) as u8,
|
||||
))
|
||||
});
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);
|
||||
methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add);
|
||||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_f32);
|
||||
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_f32);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Color3 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
r: 0f32,
|
||||
g: 0f32,
|
||||
b: 0f32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Color3 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}, {}", self.r, self.g, self.b)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for Color3 {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
Color3 {
|
||||
r: -self.r,
|
||||
g: -self.g,
|
||||
b: -self.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Color3 {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Color3 {
|
||||
r: self.r + rhs.r,
|
||||
g: self.g + rhs.g,
|
||||
b: self.b + rhs.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Color3 {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Color3 {
|
||||
r: self.r - rhs.r,
|
||||
g: self.g - rhs.g,
|
||||
b: self.b - rhs.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for Color3 {
|
||||
type Output = Color3;
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
Color3 {
|
||||
r: self.r * rhs.r,
|
||||
g: self.g * rhs.g,
|
||||
b: self.b * rhs.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<f32> for Color3 {
|
||||
type Output = Color3;
|
||||
fn mul(self, rhs: f32) -> Self::Output {
|
||||
Color3 {
|
||||
r: self.r * rhs,
|
||||
g: self.g * rhs,
|
||||
b: self.b * rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for Color3 {
|
||||
type Output = Color3;
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
Color3 {
|
||||
r: self.r / rhs.r,
|
||||
g: self.g / rhs.g,
|
||||
b: self.b / rhs.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<f32> for Color3 {
|
||||
type Output = Color3;
|
||||
fn div(self, rhs: f32) -> Self::Output {
|
||||
Color3 {
|
||||
r: self.r / rhs,
|
||||
g: self.g / rhs,
|
||||
b: self.b / rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomColor3> for Color3 {
|
||||
fn from(v: DomColor3) -> Self {
|
||||
Self {
|
||||
r: v.r,
|
||||
g: v.g,
|
||||
b: v.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color3> for DomColor3 {
|
||||
fn from(v: Color3) -> Self {
|
||||
Self {
|
||||
r: v.r,
|
||||
g: v.g,
|
||||
b: v.b,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomColor3uint8> for Color3 {
|
||||
fn from(v: DomColor3uint8) -> Self {
|
||||
Color3::from(DomColor3::from(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color3> for DomColor3uint8 {
|
||||
fn from(v: Color3) -> Self {
|
||||
DomColor3uint8::from(DomColor3::from(v))
|
||||
}
|
||||
}
|
125
crates/lune-roblox/src/datatypes/types/color_sequence.rs
Normal file
|
@ -0,0 +1,125 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{
|
||||
ColorSequence as DomColorSequence, ColorSequenceKeypoint as DomColorSequenceKeypoint,
|
||||
};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Color3, ColorSequenceKeypoint};
|
||||
|
||||
/**
|
||||
An implementation of the [ColorSequence](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequence) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `ColorSequence` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ColorSequence {
|
||||
pub(crate) keypoints: Vec<ColorSequenceKeypoint>,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for ColorSequence {
|
||||
const EXPORT_NAME: &'static str = "ColorSequence";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
type ArgsColor<'lua> = LuaUserDataRef<'lua, Color3>;
|
||||
type ArgsColors<'lua> = (LuaUserDataRef<'lua, Color3>, LuaUserDataRef<'lua, Color3>);
|
||||
type ArgsKeypoints<'lua> = Vec<LuaUserDataRef<'lua, ColorSequenceKeypoint>>;
|
||||
|
||||
let color_sequence_new = |lua, args: LuaMultiValue| {
|
||||
if let Ok(color) = ArgsColor::from_lua_multi(args.clone(), lua) {
|
||||
Ok(ColorSequence {
|
||||
keypoints: vec![
|
||||
ColorSequenceKeypoint {
|
||||
time: 0.0,
|
||||
color: *color,
|
||||
},
|
||||
ColorSequenceKeypoint {
|
||||
time: 1.0,
|
||||
color: *color,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if let Ok((c0, c1)) = ArgsColors::from_lua_multi(args.clone(), lua) {
|
||||
Ok(ColorSequence {
|
||||
keypoints: vec![
|
||||
ColorSequenceKeypoint {
|
||||
time: 0.0,
|
||||
color: *c0,
|
||||
},
|
||||
ColorSequenceKeypoint {
|
||||
time: 1.0,
|
||||
color: *c1,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) {
|
||||
Ok(ColorSequence {
|
||||
keypoints: keypoints.iter().map(|k| **k).collect(),
|
||||
})
|
||||
} else {
|
||||
// FUTURE: Better error message here using given arg types
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", color_sequence_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for ColorSequence {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ColorSequence {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (index, keypoint) in self.keypoints.iter().enumerate() {
|
||||
if index < self.keypoints.len() - 1 {
|
||||
write!(f, "{keypoint}, ")?;
|
||||
} else {
|
||||
write!(f, "{keypoint}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomColorSequence> for ColorSequence {
|
||||
fn from(v: DomColorSequence) -> Self {
|
||||
Self {
|
||||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.copied()
|
||||
.map(ColorSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorSequence> for DomColorSequence {
|
||||
fn from(v: ColorSequence) -> Self {
|
||||
Self {
|
||||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.copied()
|
||||
.map(DomColorSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::ColorSequenceKeypoint as DomColorSequenceKeypoint;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Color3};
|
||||
|
||||
/**
|
||||
An implementation of the [ColorSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequenceKeypoint) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `ColorSequenceKeypoint` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ColorSequenceKeypoint {
|
||||
pub(crate) time: f32,
|
||||
pub(crate) color: Color3,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for ColorSequenceKeypoint {
|
||||
const EXPORT_NAME: &'static str = "ColorSequenceKeypoint";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let color_sequence_keypoint_new = |_, (time, color): (f32, LuaUserDataRef<Color3>)| {
|
||||
Ok(ColorSequenceKeypoint {
|
||||
time,
|
||||
color: *color,
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", color_sequence_keypoint_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for ColorSequenceKeypoint {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Time", |_, this| Ok(this.time));
|
||||
fields.add_field_method_get("Value", |_, this| Ok(this.color));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ColorSequenceKeypoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} > {}", self.time, self.color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomColorSequenceKeypoint> for ColorSequenceKeypoint {
|
||||
fn from(v: DomColorSequenceKeypoint) -> Self {
|
||||
Self {
|
||||
time: v.time,
|
||||
color: v.color.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorSequenceKeypoint> for DomColorSequenceKeypoint {
|
||||
fn from(v: ColorSequenceKeypoint) -> Self {
|
||||
Self {
|
||||
time: v.time,
|
||||
color: v.color.into(),
|
||||
}
|
||||
}
|
||||
}
|
120
crates/lune-roblox/src/datatypes/types/content.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{Content as DomContent, ContentType};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::{exports::LuaExportsTable, instance::Instance};
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
/**
|
||||
An implementation of the [Content](https://create.roblox.com/docs/reference/engine/datatypes/Content) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the Content type as of April 2025.
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Content(ContentType);
|
||||
|
||||
impl LuaExportsTable<'_> for Content {
|
||||
const EXPORT_NAME: &'static str = "Content";
|
||||
|
||||
fn create_exports_table(lua: &'_ Lua) -> LuaResult<LuaTable<'_>> {
|
||||
let from_uri = |_, uri: String| Ok(Self(ContentType::Uri(uri)));
|
||||
|
||||
let from_object = |_, obj: LuaUserDataRef<Instance>| {
|
||||
let database = rbx_reflection_database::get();
|
||||
let instance_descriptor = database
|
||||
.classes
|
||||
.get("Instance")
|
||||
.expect("the reflection database should always have Instance in it");
|
||||
let param_descriptor = database.classes.get(obj.get_class_name()).expect(
|
||||
"you should not be able to construct an Instance that is not known to Lune",
|
||||
);
|
||||
if database.has_superclass(param_descriptor, instance_descriptor) {
|
||||
Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'"))
|
||||
} else {
|
||||
Ok(Content(ContentType::Object(obj.dom_ref)))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("none", Content(ContentType::None))?
|
||||
.with_function("fromUri", from_uri)?
|
||||
.with_function("fromObject", from_object)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Content {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("SourceType", |_, this| {
|
||||
let variant_name = match &this.0 {
|
||||
ContentType::None => "None",
|
||||
ContentType::Uri(_) => "Uri",
|
||||
ContentType::Object(_) => "Object",
|
||||
other => {
|
||||
return Err(LuaError::runtime(format!(
|
||||
"cannot get SourceType: unknown ContentType variant '{other:?}'"
|
||||
)))
|
||||
}
|
||||
};
|
||||
Ok(EnumItem::from_enum_name_and_name(
|
||||
"ContentSourceType",
|
||||
variant_name,
|
||||
))
|
||||
});
|
||||
fields.add_field_method_get("Uri", |_, this| {
|
||||
if let ContentType::Uri(uri) = &this.0 {
|
||||
Ok(Some(uri.to_owned()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
});
|
||||
fields.add_field_method_get("Object", |_, this| {
|
||||
if let ContentType::Object(referent) = &this.0 {
|
||||
Ok(Instance::new_opt(*referent))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Content {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Regardless of the actual content of the Content, Roblox just emits
|
||||
// `Content` when casting it to a string. We do not do that.
|
||||
write!(f, "Content(")?;
|
||||
match &self.0 {
|
||||
ContentType::None => write!(f, "None")?,
|
||||
ContentType::Uri(uri) => write!(f, "Uri={uri}")?,
|
||||
ContentType::Object(_) => write!(f, "Object")?,
|
||||
other => write!(f, "UnknownType({other:?})")?,
|
||||
}
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomContent> for Content {
|
||||
fn from(value: DomContent) -> Self {
|
||||
Self(value.value().clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Content> for DomContent {
|
||||
fn from(value: Content) -> Self {
|
||||
match value.0 {
|
||||
ContentType::None => Self::none(),
|
||||
ContentType::Uri(uri) => Self::from_uri(uri),
|
||||
ContentType::Object(referent) => Self::from_referent(referent),
|
||||
other => unimplemented!("unknown variant of ContentType: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
71
crates/lune-roblox/src/datatypes/types/enum.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_reflection::EnumDescriptor;
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
/**
|
||||
An implementation of the [Enum](https://create.roblox.com/docs/reference/engine/datatypes/Enum) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the Enum class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Enum {
|
||||
pub(crate) desc: &'static EnumDescriptor<'static>,
|
||||
}
|
||||
|
||||
impl Enum {
|
||||
pub(crate) fn from_name(name: impl AsRef<str>) -> Option<Self> {
|
||||
let db = rbx_reflection_database::get();
|
||||
db.enums.get(name.as_ref()).map(Enum::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Enum {
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("GetEnumItems", |_, this, ()| {
|
||||
Ok(this
|
||||
.desc
|
||||
.items
|
||||
.iter()
|
||||
.map(|(name, value)| EnumItem {
|
||||
parent: this.clone(),
|
||||
name: name.to_string(),
|
||||
value: *value,
|
||||
})
|
||||
.collect::<Vec<_>>())
|
||||
});
|
||||
methods.add_meta_method(LuaMetaMethod::Index, |_, this, name: String| {
|
||||
match EnumItem::from_enum_and_name(this, &name) {
|
||||
Some(item) => Ok(item),
|
||||
None => Err(LuaError::RuntimeError(format!(
|
||||
"The enum item '{}' does not exist for enum '{}'",
|
||||
name, this.desc.name
|
||||
))),
|
||||
}
|
||||
});
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Enum {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Enum.{}", self.desc.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Enum {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.desc.name == other.desc.name
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static EnumDescriptor<'static>> for Enum {
|
||||
fn from(value: &'static EnumDescriptor<'static>) -> Self {
|
||||
Self { desc: value }
|
||||
}
|
||||
}
|
117
crates/lune-roblox/src/datatypes/types/enum_item.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::EnumItem as DomEnumItem;
|
||||
|
||||
use super::{super::*, Enum};
|
||||
|
||||
/**
|
||||
An implementation of the [EnumItem](https://create.roblox.com/docs/reference/engine/datatypes/EnumItem) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `EnumItem` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnumItem {
|
||||
pub(crate) parent: Enum,
|
||||
pub(crate) name: String,
|
||||
pub(crate) value: u32,
|
||||
}
|
||||
|
||||
impl EnumItem {
|
||||
pub(crate) fn from_enum_and_name(parent: &Enum, name: impl AsRef<str>) -> Option<Self> {
|
||||
let enum_name = name.as_ref();
|
||||
parent.desc.items.iter().find_map(|(name, v)| {
|
||||
if *name == enum_name {
|
||||
Some(Self {
|
||||
parent: parent.clone(),
|
||||
name: enum_name.to_string(),
|
||||
value: *v,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn from_enum_and_value(parent: &Enum, value: u32) -> Option<Self> {
|
||||
parent.desc.items.iter().find_map(|(name, v)| {
|
||||
if *v == value {
|
||||
Some(Self {
|
||||
parent: parent.clone(),
|
||||
name: name.to_string(),
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn from_enum_name_and_name(
|
||||
enum_name: impl AsRef<str>,
|
||||
name: impl AsRef<str>,
|
||||
) -> Option<Self> {
|
||||
let parent = Enum::from_name(enum_name)?;
|
||||
Self::from_enum_and_name(&parent, name)
|
||||
}
|
||||
|
||||
pub(crate) fn from_enum_name_and_value(enum_name: impl AsRef<str>, value: u32) -> Option<Self> {
|
||||
let parent = Enum::from_name(enum_name)?;
|
||||
Self::from_enum_and_value(&parent, value)
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for EnumItem {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Name", |_, this| Ok(this.name.clone()));
|
||||
fields.add_field_method_get("Value", |_, this| Ok(this.value));
|
||||
fields.add_field_method_get("EnumType", |_, this| Ok(this.parent.clone()));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for EnumItem {
|
||||
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
||||
if let LuaValue::UserData(ud) = value {
|
||||
Ok(ud.borrow::<EnumItem>()?.to_owned())
|
||||
} else {
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "EnumItem",
|
||||
message: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EnumItem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}.{}", self.parent, self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for EnumItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.parent == other.parent && self.value == other.value
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EnumItem> for DomEnumItem {
|
||||
fn from(v: EnumItem) -> Self {
|
||||
DomEnumItem {
|
||||
ty: v.parent.desc.name.to_string(),
|
||||
value: v.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomEnumItem> for EnumItem {
|
||||
fn from(value: DomEnumItem) -> Self {
|
||||
EnumItem::from_enum_name_and_value(value.ty, value.value)
|
||||
.expect("cannot convert rbx_type::EnumItem with unknown type into EnumItem")
|
||||
}
|
||||
}
|
41
crates/lune-roblox/src/datatypes/types/enums.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use super::{super::*, Enum};
|
||||
|
||||
/**
|
||||
An implementation of the [Enums](https://create.roblox.com/docs/reference/engine/datatypes/Enums) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the Enums class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Enums;
|
||||
|
||||
impl LuaUserData for Enums {
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("GetEnums", |_, _, ()| {
|
||||
let db = rbx_reflection_database::get();
|
||||
Ok(db.enums.values().map(Enum::from).collect::<Vec<_>>())
|
||||
});
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Index,
|
||||
|_, _, name: String| match Enum::from_name(&name) {
|
||||
Some(e) => Ok(e),
|
||||
None => Err(LuaError::RuntimeError(format!(
|
||||
"The enum '{name}' does not exist",
|
||||
))),
|
||||
},
|
||||
);
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Enums {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Enum")
|
||||
}
|
||||
}
|
143
crates/lune-roblox/src/datatypes/types/faces.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
#![allow(clippy::struct_excessive_bools)]
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Faces as DomFaces;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
/**
|
||||
An implementation of the [Faces](https://create.roblox.com/docs/reference/engine/datatypes/Faces) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the Faces class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Faces {
|
||||
pub(crate) right: bool,
|
||||
pub(crate) top: bool,
|
||||
pub(crate) back: bool,
|
||||
pub(crate) left: bool,
|
||||
pub(crate) bottom: bool,
|
||||
pub(crate) front: bool,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for Faces {
|
||||
const EXPORT_NAME: &'static str = "Faces";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let faces_new = |_, args: LuaMultiValue| {
|
||||
let mut right = false;
|
||||
let mut top = false;
|
||||
let mut back = false;
|
||||
let mut left = false;
|
||||
let mut bottom = false;
|
||||
let mut front = false;
|
||||
|
||||
let mut check = |e: &EnumItem| {
|
||||
if e.parent.desc.name == "NormalId" {
|
||||
match &e.name {
|
||||
name if name == "Right" => right = true,
|
||||
name if name == "Top" => top = true,
|
||||
name if name == "Back" => back = true,
|
||||
name if name == "Left" => left = true,
|
||||
name if name == "Bottom" => bottom = true,
|
||||
name if name == "Front" => front = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (index, arg) in args.into_iter().enumerate() {
|
||||
if let LuaValue::UserData(u) = arg {
|
||||
if let Ok(e) = u.borrow::<EnumItem>() {
|
||||
check(&e);
|
||||
} else {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected argument #{index} to be an EnumItem, got userdata",
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected argument #{} to be an EnumItem, got {}",
|
||||
index,
|
||||
arg.type_name()
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Faces {
|
||||
right,
|
||||
top,
|
||||
back,
|
||||
left,
|
||||
bottom,
|
||||
front,
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", faces_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Faces {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Right", |_, this| Ok(this.right));
|
||||
fields.add_field_method_get("Top", |_, this| Ok(this.top));
|
||||
fields.add_field_method_get("Back", |_, this| Ok(this.back));
|
||||
fields.add_field_method_get("Left", |_, this| Ok(this.left));
|
||||
fields.add_field_method_get("Bottom", |_, this| Ok(this.bottom));
|
||||
fields.add_field_method_get("Front", |_, this| Ok(this.front));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Faces {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let write = make_list_writer();
|
||||
write(f, self.right, "Right")?;
|
||||
write(f, self.top, "Top")?;
|
||||
write(f, self.back, "Back")?;
|
||||
write(f, self.left, "Left")?;
|
||||
write(f, self.bottom, "Bottom")?;
|
||||
write(f, self.front, "Front")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomFaces> for Faces {
|
||||
fn from(v: DomFaces) -> Self {
|
||||
let bits = v.bits();
|
||||
Self {
|
||||
right: (bits & 1) == 1,
|
||||
top: ((bits >> 1) & 1) == 1,
|
||||
back: ((bits >> 2) & 1) == 1,
|
||||
left: ((bits >> 3) & 1) == 1,
|
||||
bottom: ((bits >> 4) & 1) == 1,
|
||||
front: ((bits >> 5) & 1) == 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Faces> for DomFaces {
|
||||
fn from(v: Faces) -> Self {
|
||||
let mut bits = 0;
|
||||
bits += v.right as u8;
|
||||
bits += (v.top as u8) << 1;
|
||||
bits += (v.back as u8) << 2;
|
||||
bits += (v.left as u8) << 3;
|
||||
bits += (v.bottom as u8) << 4;
|
||||
bits += (v.front as u8) << 5;
|
||||
DomFaces::from_bits(bits).expect("Invalid bits")
|
||||
}
|
||||
}
|
469
crates/lune-roblox/src/datatypes/types/font.rs
Normal file
|
@ -0,0 +1,469 @@
|
|||
use core::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{
|
||||
Font as DomFont, FontStyle as DomFontStyle, FontWeight as DomFontWeight,
|
||||
};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
/**
|
||||
An implementation of the [Font](https://create.roblox.com/docs/reference/engine/datatypes/Font) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the Font class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Font {
|
||||
pub(crate) family: String,
|
||||
pub(crate) weight: FontWeight,
|
||||
pub(crate) style: FontStyle,
|
||||
pub(crate) cached_id: Option<String>,
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub(crate) fn from_enum_item(material_enum_item: &EnumItem) -> Option<Font> {
|
||||
FONT_ENUM_MAP
|
||||
.iter()
|
||||
.find(|props| props.0 == material_enum_item.name && props.1.is_some())
|
||||
.map(|props| props.1.as_ref().unwrap())
|
||||
.map(|props| Font {
|
||||
family: props.0.to_string(),
|
||||
weight: props.1,
|
||||
style: props.2,
|
||||
cached_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for Font {
|
||||
const EXPORT_NAME: &'static str = "Font";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let font_from_enum = |_, value: LuaUserDataRef<EnumItem>| {
|
||||
if value.parent.desc.name == "Font" {
|
||||
match Font::from_enum_item(&value) {
|
||||
Some(props) => Ok(props),
|
||||
None => Err(LuaError::RuntimeError(format!(
|
||||
"Found unknown Font '{}'",
|
||||
value.name
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Expected argument #1 to be a Font, got {}",
|
||||
value.parent.desc.name
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let font_from_name =
|
||||
|_, (file, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
|
||||
Ok(Font {
|
||||
family: format!("rbxasset://fonts/families/{file}.json"),
|
||||
weight: weight.unwrap_or_default(),
|
||||
style: style.unwrap_or_default(),
|
||||
cached_id: None,
|
||||
})
|
||||
};
|
||||
|
||||
let font_from_id =
|
||||
|_, (id, weight, style): (i32, Option<FontWeight>, Option<FontStyle>)| {
|
||||
Ok(Font {
|
||||
family: format!("rbxassetid://{id}"),
|
||||
weight: weight.unwrap_or_default(),
|
||||
style: style.unwrap_or_default(),
|
||||
cached_id: None,
|
||||
})
|
||||
};
|
||||
|
||||
let font_new =
|
||||
|_, (family, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
|
||||
Ok(Font {
|
||||
family,
|
||||
weight: weight.unwrap_or_default(),
|
||||
style: style.unwrap_or_default(),
|
||||
cached_id: None,
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("fromEnum", font_from_enum)?
|
||||
.with_function("fromName", font_from_name)?
|
||||
.with_function("fromId", font_from_id)?
|
||||
.with_function("new", font_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Font {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
// Getters
|
||||
fields.add_field_method_get("Family", |_, this| Ok(this.family.clone()));
|
||||
fields.add_field_method_get("Weight", |_, this| Ok(this.weight));
|
||||
fields.add_field_method_get("Style", |_, this| Ok(this.style));
|
||||
fields.add_field_method_get("Bold", |_, this| Ok(this.weight.as_u16() >= 600));
|
||||
// Setters
|
||||
fields.add_field_method_set("Weight", |_, this, value: FontWeight| {
|
||||
this.weight = value;
|
||||
Ok(())
|
||||
});
|
||||
fields.add_field_method_set("Style", |_, this, value: FontStyle| {
|
||||
this.style = value;
|
||||
Ok(())
|
||||
});
|
||||
fields.add_field_method_set("Bold", |_, this, value: bool| {
|
||||
if value {
|
||||
this.weight = FontWeight::Bold;
|
||||
} else {
|
||||
this.weight = FontWeight::Regular;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Font {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}, {}", self.family, self.weight, self.style)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomFont> for Font {
|
||||
fn from(v: DomFont) -> Self {
|
||||
Self {
|
||||
family: v.family,
|
||||
weight: v.weight.into(),
|
||||
style: v.style.into(),
|
||||
cached_id: v.cached_face_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Font> for DomFont {
|
||||
fn from(v: Font) -> Self {
|
||||
DomFont {
|
||||
family: v.family,
|
||||
weight: v.weight.into(),
|
||||
style: v.style.into(),
|
||||
cached_face_id: v.cached_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomFontWeight> for FontWeight {
|
||||
fn from(v: DomFontWeight) -> Self {
|
||||
FontWeight::from_u16(v.as_u16()).expect("Missing font weight")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontWeight> for DomFontWeight {
|
||||
fn from(v: FontWeight) -> Self {
|
||||
DomFontWeight::from_u16(v.as_u16()).expect("Missing rbx font weight")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomFontStyle> for FontStyle {
|
||||
fn from(v: DomFontStyle) -> Self {
|
||||
FontStyle::from_u8(v.as_u8()).expect("Missing font weight")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FontStyle> for DomFontStyle {
|
||||
fn from(v: FontStyle) -> Self {
|
||||
DomFontStyle::from_u8(v.as_u8()).expect("Missing rbx font weight")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
NOTE: The font code below is all generated using the
|
||||
font_enum_map script in the scripts dir next to src,
|
||||
which can be ran in the Roblox Studio command bar
|
||||
|
||||
*/
|
||||
|
||||
type FontData = (&'static str, FontWeight, FontStyle);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum FontWeight {
|
||||
Thin,
|
||||
ExtraLight,
|
||||
Light,
|
||||
Regular,
|
||||
Medium,
|
||||
SemiBold,
|
||||
Bold,
|
||||
ExtraBold,
|
||||
Heavy,
|
||||
}
|
||||
|
||||
impl FontWeight {
|
||||
pub(crate) fn as_u16(self) -> u16 {
|
||||
match self {
|
||||
Self::Thin => 100,
|
||||
Self::ExtraLight => 200,
|
||||
Self::Light => 300,
|
||||
Self::Regular => 400,
|
||||
Self::Medium => 500,
|
||||
Self::SemiBold => 600,
|
||||
Self::Bold => 700,
|
||||
Self::ExtraBold => 800,
|
||||
Self::Heavy => 900,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_u16(n: u16) -> Option<Self> {
|
||||
match n {
|
||||
100 => Some(Self::Thin),
|
||||
200 => Some(Self::ExtraLight),
|
||||
300 => Some(Self::Light),
|
||||
400 => Some(Self::Regular),
|
||||
500 => Some(Self::Medium),
|
||||
600 => Some(Self::SemiBold),
|
||||
700 => Some(Self::Bold),
|
||||
800 => Some(Self::ExtraBold),
|
||||
900 => Some(Self::Heavy),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FontWeight {
|
||||
fn default() -> Self {
|
||||
Self::Regular
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for FontWeight {
|
||||
type Err = &'static str;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"Thin" => Ok(Self::Thin),
|
||||
"ExtraLight" => Ok(Self::ExtraLight),
|
||||
"Light" => Ok(Self::Light),
|
||||
"Regular" => Ok(Self::Regular),
|
||||
"Medium" => Ok(Self::Medium),
|
||||
"SemiBold" => Ok(Self::SemiBold),
|
||||
"Bold" => Ok(Self::Bold),
|
||||
"ExtraBold" => Ok(Self::ExtraBold),
|
||||
"Heavy" => Ok(Self::Heavy),
|
||||
_ => Err("Unknown FontWeight"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FontWeight {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Thin => "Thin",
|
||||
Self::ExtraLight => "ExtraLight",
|
||||
Self::Light => "Light",
|
||||
Self::Regular => "Regular",
|
||||
Self::Medium => "Medium",
|
||||
Self::SemiBold => "SemiBold",
|
||||
Self::Bold => "Bold",
|
||||
Self::ExtraBold => "ExtraBold",
|
||||
Self::Heavy => "Heavy",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for FontWeight {
|
||||
fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
||||
let mut message = None;
|
||||
if let LuaValue::UserData(ud) = &lua_value {
|
||||
let value = ud.borrow::<EnumItem>()?;
|
||||
if value.parent.desc.name == "FontWeight" {
|
||||
if let Ok(value) = FontWeight::from_str(&value.name) {
|
||||
return Ok(value);
|
||||
}
|
||||
message = Some(format!(
|
||||
"Found unknown Enum.FontWeight value '{}'",
|
||||
value.name
|
||||
));
|
||||
} else {
|
||||
message = Some(format!(
|
||||
"Expected Enum.FontWeight, got Enum.{}",
|
||||
value.parent.desc.name
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: lua_value.type_name(),
|
||||
to: "Enum.FontWeight",
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> IntoLua<'lua> for FontWeight {
|
||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
||||
match EnumItem::from_enum_name_and_name("FontWeight", self.to_string()) {
|
||||
Some(enum_item) => Ok(LuaValue::UserData(lua.create_userdata(enum_item)?)),
|
||||
None => Err(LuaError::ToLuaConversionError {
|
||||
from: "FontWeight",
|
||||
to: "EnumItem",
|
||||
message: Some(format!("Found unknown Enum.FontWeight value '{self}'")),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum FontStyle {
|
||||
Normal,
|
||||
Italic,
|
||||
}
|
||||
|
||||
impl FontStyle {
|
||||
pub(crate) fn as_u8(self) -> u8 {
|
||||
match self {
|
||||
Self::Normal => 0,
|
||||
Self::Italic => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_u8(n: u8) -> Option<Self> {
|
||||
match n {
|
||||
0 => Some(Self::Normal),
|
||||
1 => Some(Self::Italic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FontStyle {
|
||||
fn default() -> Self {
|
||||
Self::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for FontStyle {
|
||||
type Err = &'static str;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"Normal" => Ok(Self::Normal),
|
||||
"Italic" => Ok(Self::Italic),
|
||||
_ => Err("Unknown FontStyle"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FontStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::Normal => "Normal",
|
||||
Self::Italic => "Italic",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> FromLua<'lua> for FontStyle {
|
||||
fn from_lua(lua_value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
|
||||
let mut message = None;
|
||||
if let LuaValue::UserData(ud) = &lua_value {
|
||||
let value = ud.borrow::<EnumItem>()?;
|
||||
if value.parent.desc.name == "FontStyle" {
|
||||
if let Ok(value) = FontStyle::from_str(&value.name) {
|
||||
return Ok(value);
|
||||
}
|
||||
message = Some(format!(
|
||||
"Found unknown Enum.FontStyle value '{}'",
|
||||
value.name
|
||||
));
|
||||
} else {
|
||||
message = Some(format!(
|
||||
"Expected Enum.FontStyle, got Enum.{}",
|
||||
value.parent.desc.name
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: lua_value.type_name(),
|
||||
to: "Enum.FontStyle",
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'lua> IntoLua<'lua> for FontStyle {
|
||||
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
|
||||
match EnumItem::from_enum_name_and_name("FontStyle", self.to_string()) {
|
||||
Some(enum_item) => Ok(LuaValue::UserData(lua.create_userdata(enum_item)?)),
|
||||
None => Err(LuaError::ToLuaConversionError {
|
||||
from: "FontStyle",
|
||||
to: "EnumItem",
|
||||
message: Some(format!("Found unknown Enum.FontStyle value '{self}'")),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const FONT_ENUM_MAP: &[(&str, Option<FontData>)] = &[
|
||||
("Legacy", Some(("rbxasset://fonts/families/LegacyArial.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Arial", Some(("rbxasset://fonts/families/Arial.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("ArialBold", Some(("rbxasset://fonts/families/Arial.json", FontWeight::Bold, FontStyle::Normal))),
|
||||
("SourceSans", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("SourceSansBold", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Bold, FontStyle::Normal))),
|
||||
("SourceSansSemibold", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::SemiBold, FontStyle::Normal))),
|
||||
("SourceSansLight", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Light, FontStyle::Normal))),
|
||||
("SourceSansItalic", Some(("rbxasset://fonts/families/SourceSansPro.json", FontWeight::Regular, FontStyle::Italic))),
|
||||
("Bodoni", Some(("rbxasset://fonts/families/AccanthisADFStd.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Garamond", Some(("rbxasset://fonts/families/Guru.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Cartoon", Some(("rbxasset://fonts/families/ComicNeueAngular.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Code", Some(("rbxasset://fonts/families/Inconsolata.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Highway", Some(("rbxasset://fonts/families/HighwayGothic.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("SciFi", Some(("rbxasset://fonts/families/Zekton.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Arcade", Some(("rbxasset://fonts/families/PressStart2P.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Fantasy", Some(("rbxasset://fonts/families/Balthazar.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Antique", Some(("rbxasset://fonts/families/RomanAntique.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Gotham", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("GothamMedium", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Medium, FontStyle::Normal))),
|
||||
("GothamBold", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Bold, FontStyle::Normal))),
|
||||
("GothamBlack", Some(("rbxasset://fonts/families/GothamSSm.json", FontWeight::Heavy, FontStyle::Normal))),
|
||||
("AmaticSC", Some(("rbxasset://fonts/families/AmaticSC.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Bangers", Some(("rbxasset://fonts/families/Bangers.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Creepster", Some(("rbxasset://fonts/families/Creepster.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("DenkOne", Some(("rbxasset://fonts/families/DenkOne.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Fondamento", Some(("rbxasset://fonts/families/Fondamento.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("FredokaOne", Some(("rbxasset://fonts/families/FredokaOne.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("GrenzeGotisch", Some(("rbxasset://fonts/families/GrenzeGotisch.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("IndieFlower", Some(("rbxasset://fonts/families/IndieFlower.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("JosefinSans", Some(("rbxasset://fonts/families/JosefinSans.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Jura", Some(("rbxasset://fonts/families/Jura.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Kalam", Some(("rbxasset://fonts/families/Kalam.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("LuckiestGuy", Some(("rbxasset://fonts/families/LuckiestGuy.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Merriweather", Some(("rbxasset://fonts/families/Merriweather.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Michroma", Some(("rbxasset://fonts/families/Michroma.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Nunito", Some(("rbxasset://fonts/families/Nunito.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Oswald", Some(("rbxasset://fonts/families/Oswald.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("PatrickHand", Some(("rbxasset://fonts/families/PatrickHand.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("PermanentMarker", Some(("rbxasset://fonts/families/PermanentMarker.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Roboto", Some(("rbxasset://fonts/families/Roboto.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("RobotoCondensed", Some(("rbxasset://fonts/families/RobotoCondensed.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("RobotoMono", Some(("rbxasset://fonts/families/RobotoMono.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Sarpanch", Some(("rbxasset://fonts/families/Sarpanch.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("SpecialElite", Some(("rbxasset://fonts/families/SpecialElite.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("TitilliumWeb", Some(("rbxasset://fonts/families/TitilliumWeb.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Ubuntu", Some(("rbxasset://fonts/families/Ubuntu.json", FontWeight::Regular, FontStyle::Normal))),
|
||||
("Unknown", None),
|
||||
];
|
53
crates/lune-roblox/src/datatypes/types/mod.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
mod axes;
|
||||
mod brick_color;
|
||||
mod cframe;
|
||||
mod color3;
|
||||
mod color_sequence;
|
||||
mod color_sequence_keypoint;
|
||||
mod content;
|
||||
mod r#enum;
|
||||
mod r#enum_item;
|
||||
mod r#enums;
|
||||
mod faces;
|
||||
mod font;
|
||||
mod number_range;
|
||||
mod number_sequence;
|
||||
mod number_sequence_keypoint;
|
||||
mod physical_properties;
|
||||
mod ray;
|
||||
mod rect;
|
||||
mod region3;
|
||||
mod region3int16;
|
||||
mod udim;
|
||||
mod udim2;
|
||||
mod vector2;
|
||||
mod vector2int16;
|
||||
mod vector3;
|
||||
mod vector3int16;
|
||||
|
||||
pub use axes::Axes;
|
||||
pub use brick_color::BrickColor;
|
||||
pub use cframe::CFrame;
|
||||
pub use color3::Color3;
|
||||
pub use color_sequence::ColorSequence;
|
||||
pub use color_sequence_keypoint::ColorSequenceKeypoint;
|
||||
pub use content::Content;
|
||||
pub use faces::Faces;
|
||||
pub use font::Font;
|
||||
pub use number_range::NumberRange;
|
||||
pub use number_sequence::NumberSequence;
|
||||
pub use number_sequence_keypoint::NumberSequenceKeypoint;
|
||||
pub use physical_properties::PhysicalProperties;
|
||||
pub use r#enum::Enum;
|
||||
pub use r#enum_item::EnumItem;
|
||||
pub use r#enums::Enums;
|
||||
pub use ray::Ray;
|
||||
pub use rect::Rect;
|
||||
pub use region3::Region3;
|
||||
pub use region3int16::Region3int16;
|
||||
pub use udim::UDim;
|
||||
pub use udim2::UDim2;
|
||||
pub use vector2::Vector2;
|
||||
pub use vector2int16::Vector2int16;
|
||||
pub use vector3::Vector3;
|
||||
pub use vector3int16::Vector3int16;
|
77
crates/lune-roblox/src/datatypes/types/number_range.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::NumberRange as DomNumberRange;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [NumberRange](https://create.roblox.com/docs/reference/engine/datatypes/NumberRange) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `NumberRange` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct NumberRange {
|
||||
pub(crate) min: f32,
|
||||
pub(crate) max: f32,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for NumberRange {
|
||||
const EXPORT_NAME: &'static str = "NumberRange";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let number_range_new = |_, (min, max): (f32, Option<f32>)| {
|
||||
Ok(match max {
|
||||
Some(max) => NumberRange {
|
||||
min: min.min(max),
|
||||
max: min.max(max),
|
||||
},
|
||||
None => NumberRange { min, max: min },
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", number_range_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for NumberRange {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Min", |_, this| Ok(this.min));
|
||||
fields.add_field_method_get("Max", |_, this| Ok(this.max));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for NumberRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", self.min, self.max)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomNumberRange> for NumberRange {
|
||||
fn from(v: DomNumberRange) -> Self {
|
||||
Self {
|
||||
min: v.min,
|
||||
max: v.max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NumberRange> for DomNumberRange {
|
||||
fn from(v: NumberRange) -> Self {
|
||||
Self {
|
||||
min: v.min,
|
||||
max: v.max,
|
||||
}
|
||||
}
|
||||
}
|
129
crates/lune-roblox/src/datatypes/types/number_sequence.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{
|
||||
NumberSequence as DomNumberSequence, NumberSequenceKeypoint as DomNumberSequenceKeypoint,
|
||||
};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, NumberSequenceKeypoint};
|
||||
|
||||
/**
|
||||
An implementation of the [NumberSequence](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequence) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `NumberSequence` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NumberSequence {
|
||||
pub(crate) keypoints: Vec<NumberSequenceKeypoint>,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for NumberSequence {
|
||||
const EXPORT_NAME: &'static str = "NumberSequence";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
type ArgsColor = f32;
|
||||
type ArgsColors = (f32, f32);
|
||||
type ArgsKeypoints<'lua> = Vec<LuaUserDataRef<'lua, NumberSequenceKeypoint>>;
|
||||
|
||||
let number_sequence_new = |lua, args: LuaMultiValue| {
|
||||
if let Ok(value) = ArgsColor::from_lua_multi(args.clone(), lua) {
|
||||
Ok(NumberSequence {
|
||||
keypoints: vec![
|
||||
NumberSequenceKeypoint {
|
||||
time: 0.0,
|
||||
value,
|
||||
envelope: 0.0,
|
||||
},
|
||||
NumberSequenceKeypoint {
|
||||
time: 1.0,
|
||||
value,
|
||||
envelope: 0.0,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if let Ok((v0, v1)) = ArgsColors::from_lua_multi(args.clone(), lua) {
|
||||
Ok(NumberSequence {
|
||||
keypoints: vec![
|
||||
NumberSequenceKeypoint {
|
||||
time: 0.0,
|
||||
value: v0,
|
||||
envelope: 0.0,
|
||||
},
|
||||
NumberSequenceKeypoint {
|
||||
time: 1.0,
|
||||
value: v1,
|
||||
envelope: 0.0,
|
||||
},
|
||||
],
|
||||
})
|
||||
} else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) {
|
||||
Ok(NumberSequence {
|
||||
keypoints: keypoints.iter().map(|k| **k).collect(),
|
||||
})
|
||||
} else {
|
||||
// FUTURE: Better error message here using given arg types
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", number_sequence_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for NumberSequence {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Keypoints", |_, this| Ok(this.keypoints.clone()));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for NumberSequence {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for (index, keypoint) in self.keypoints.iter().enumerate() {
|
||||
if index < self.keypoints.len() - 1 {
|
||||
write!(f, "{keypoint}, ")?;
|
||||
} else {
|
||||
write!(f, "{keypoint}")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomNumberSequence> for NumberSequence {
|
||||
fn from(v: DomNumberSequence) -> Self {
|
||||
Self {
|
||||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.copied()
|
||||
.map(NumberSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NumberSequence> for DomNumberSequence {
|
||||
fn from(v: NumberSequence) -> Self {
|
||||
Self {
|
||||
keypoints: v
|
||||
.keypoints
|
||||
.iter()
|
||||
.copied()
|
||||
.map(DomNumberSequenceKeypoint::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::NumberSequenceKeypoint as DomNumberSequenceKeypoint;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [NumberSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequenceKeypoint) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `NumberSequenceKeypoint` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct NumberSequenceKeypoint {
|
||||
pub(crate) time: f32,
|
||||
pub(crate) value: f32,
|
||||
pub(crate) envelope: f32,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for NumberSequenceKeypoint {
|
||||
const EXPORT_NAME: &'static str = "NumberSequenceKeypoint";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let number_sequence_keypoint_new = |_, (time, value, envelope): (f32, f32, Option<f32>)| {
|
||||
Ok(NumberSequenceKeypoint {
|
||||
time,
|
||||
value,
|
||||
envelope: envelope.unwrap_or_default(),
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", number_sequence_keypoint_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for NumberSequenceKeypoint {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Time", |_, this| Ok(this.time));
|
||||
fields.add_field_method_get("Value", |_, this| Ok(this.value));
|
||||
fields.add_field_method_get("Envelope", |_, this| Ok(this.envelope));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for NumberSequenceKeypoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{} > {}", self.time, self.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomNumberSequenceKeypoint> for NumberSequenceKeypoint {
|
||||
fn from(v: DomNumberSequenceKeypoint) -> Self {
|
||||
Self {
|
||||
time: v.time,
|
||||
value: v.value,
|
||||
envelope: v.envelope,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NumberSequenceKeypoint> for DomNumberSequenceKeypoint {
|
||||
fn from(v: NumberSequenceKeypoint) -> Self {
|
||||
Self {
|
||||
time: v.time,
|
||||
value: v.value,
|
||||
envelope: v.envelope,
|
||||
}
|
||||
}
|
||||
}
|
188
crates/lune-roblox/src/datatypes/types/physical_properties.rs
Normal file
|
@ -0,0 +1,188 @@
|
|||
use core::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::CustomPhysicalProperties as DomCustomPhysicalProperties;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
/**
|
||||
An implementation of the [PhysicalProperties](https://create.roblox.com/docs/reference/engine/datatypes/PhysicalProperties) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `PhysicalProperties` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct PhysicalProperties {
|
||||
pub(crate) density: f32,
|
||||
pub(crate) friction: f32,
|
||||
pub(crate) friction_weight: f32,
|
||||
pub(crate) elasticity: f32,
|
||||
pub(crate) elasticity_weight: f32,
|
||||
}
|
||||
|
||||
impl PhysicalProperties {
|
||||
pub(crate) fn from_material(material_enum_item: &EnumItem) -> Option<PhysicalProperties> {
|
||||
MATERIAL_ENUM_MAP
|
||||
.iter()
|
||||
.find(|props| props.0 == material_enum_item.name)
|
||||
.map(|props| PhysicalProperties {
|
||||
density: props.1,
|
||||
friction: props.2,
|
||||
elasticity: props.3,
|
||||
friction_weight: props.4,
|
||||
elasticity_weight: props.5,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for PhysicalProperties {
|
||||
const EXPORT_NAME: &'static str = "PhysicalProperties";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
type ArgsMaterial<'lua> = LuaUserDataRef<'lua, EnumItem>;
|
||||
type ArgsNumbers = (f32, f32, f32, Option<f32>, Option<f32>);
|
||||
|
||||
let physical_properties_new = |lua, args: LuaMultiValue| {
|
||||
if let Ok(value) = ArgsMaterial::from_lua_multi(args.clone(), lua) {
|
||||
if value.parent.desc.name == "Material" {
|
||||
match PhysicalProperties::from_material(&value) {
|
||||
Some(props) => Ok(props),
|
||||
None => Err(LuaError::RuntimeError(format!(
|
||||
"Found unknown Material '{}'",
|
||||
value.name
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Expected argument #1 to be a Material, got {}",
|
||||
value.parent.desc.name
|
||||
)))
|
||||
}
|
||||
} else if let Ok((density, friction, elasticity, friction_weight, elasticity_weight)) =
|
||||
ArgsNumbers::from_lua_multi(args, lua)
|
||||
{
|
||||
Ok(PhysicalProperties {
|
||||
density,
|
||||
friction,
|
||||
friction_weight: friction_weight.unwrap_or(1.0),
|
||||
elasticity,
|
||||
elasticity_weight: elasticity_weight.unwrap_or(1.0),
|
||||
})
|
||||
} else {
|
||||
// FUTURE: Better error message here using given arg types
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", physical_properties_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for PhysicalProperties {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Density", |_, this| Ok(this.density));
|
||||
fields.add_field_method_get("Friction", |_, this| Ok(this.friction));
|
||||
fields.add_field_method_get("FrictionWeight", |_, this| Ok(this.friction_weight));
|
||||
fields.add_field_method_get("Elasticity", |_, this| Ok(this.elasticity));
|
||||
fields.add_field_method_get("ElasticityWeight", |_, this| Ok(this.elasticity_weight));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PhysicalProperties {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}, {}, {}, {}, {}",
|
||||
self.density,
|
||||
self.friction,
|
||||
self.elasticity,
|
||||
self.friction_weight,
|
||||
self.elasticity_weight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomCustomPhysicalProperties> for PhysicalProperties {
|
||||
fn from(v: DomCustomPhysicalProperties) -> Self {
|
||||
Self {
|
||||
density: v.density,
|
||||
friction: v.friction,
|
||||
friction_weight: v.friction_weight,
|
||||
elasticity: v.elasticity,
|
||||
elasticity_weight: v.elasticity_weight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PhysicalProperties> for DomCustomPhysicalProperties {
|
||||
fn from(v: PhysicalProperties) -> Self {
|
||||
DomCustomPhysicalProperties {
|
||||
density: v.density,
|
||||
friction: v.friction,
|
||||
friction_weight: v.friction_weight,
|
||||
elasticity: v.elasticity,
|
||||
elasticity_weight: v.elasticity_weight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
NOTE: The material definitions below are generated using the
|
||||
physical_properties_enum_map script in the scripts dir next
|
||||
to src, which can be ran in the Roblox Studio command bar
|
||||
|
||||
*/
|
||||
|
||||
#[rustfmt::skip]
|
||||
const MATERIAL_ENUM_MAP: &[(&str, f32, f32, f32, f32, f32)] = &[
|
||||
("Plastic", 0.70, 0.30, 0.50, 1.00, 1.00),
|
||||
("Wood", 0.35, 0.48, 0.20, 1.00, 1.00),
|
||||
("Slate", 2.69, 0.40, 0.20, 1.00, 1.00),
|
||||
("Concrete", 2.40, 0.70, 0.20, 0.30, 1.00),
|
||||
("CorrodedMetal", 7.85, 0.70, 0.20, 1.00, 1.00),
|
||||
("DiamondPlate", 7.85, 0.35, 0.25, 1.00, 1.00),
|
||||
("Foil", 2.70, 0.40, 0.25, 1.00, 1.00),
|
||||
("Grass", 0.90, 0.40, 0.10, 1.00, 1.50),
|
||||
("Ice", 0.92, 0.02, 0.15, 3.00, 1.00),
|
||||
("Marble", 2.56, 0.20, 0.17, 1.00, 1.00),
|
||||
("Granite", 2.69, 0.40, 0.20, 1.00, 1.00),
|
||||
("Brick", 1.92, 0.80, 0.15, 0.30, 1.00),
|
||||
("Pebble", 2.40, 0.40, 0.17, 1.00, 1.50),
|
||||
("Sand", 1.60, 0.50, 0.05, 5.00, 2.50),
|
||||
("Fabric", 0.70, 0.35, 0.05, 1.00, 1.00),
|
||||
("SmoothPlastic", 0.70, 0.20, 0.50, 1.00, 1.00),
|
||||
("Metal", 7.85, 0.40, 0.25, 1.00, 1.00),
|
||||
("WoodPlanks", 0.35, 0.48, 0.20, 1.00, 1.00),
|
||||
("Cobblestone", 2.69, 0.50, 0.17, 1.00, 1.00),
|
||||
("Air", 0.01, 0.01, 0.01, 1.00, 1.00),
|
||||
("Water", 1.00, 0.00, 0.01, 1.00, 1.00),
|
||||
("Rock", 2.69, 0.50, 0.17, 1.00, 1.00),
|
||||
("Glacier", 0.92, 0.05, 0.15, 2.00, 1.00),
|
||||
("Snow", 0.90, 0.30, 0.03, 3.00, 4.00),
|
||||
("Sandstone", 2.69, 0.50, 0.15, 5.00, 1.00),
|
||||
("Mud", 0.90, 0.30, 0.07, 3.00, 4.00),
|
||||
("Basalt", 2.69, 0.70, 0.15, 0.30, 1.00),
|
||||
("Ground", 0.90, 0.45, 0.10, 1.00, 1.00),
|
||||
("CrackedLava", 2.69, 0.65, 0.15, 1.00, 1.00),
|
||||
("Neon", 0.70, 0.30, 0.20, 1.00, 1.00),
|
||||
("Glass", 2.40, 0.25, 0.20, 1.00, 1.00),
|
||||
("Asphalt", 2.36, 0.80, 0.20, 0.30, 1.00),
|
||||
("LeafyGrass", 0.90, 0.40, 0.10, 2.00, 2.00),
|
||||
("Salt", 2.16, 0.50, 0.05, 1.00, 1.00),
|
||||
("Limestone", 2.69, 0.50, 0.15, 1.00, 1.00),
|
||||
("Pavement", 2.69, 0.50, 0.17, 0.30, 1.00),
|
||||
("ForceField", 2.40, 0.25, 0.20, 1.00, 1.00),
|
||||
];
|
102
crates/lune-roblox/src/datatypes/types/ray.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
use core::fmt;
|
||||
|
||||
use glam::Vec3;
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Ray as DomRay;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Vector3};
|
||||
|
||||
/**
|
||||
An implementation of the [Ray](https://create.roblox.com/docs/reference/engine/datatypes/Ray)
|
||||
Roblox datatype, backed by [`glam::Vec3`].
|
||||
|
||||
This implements all documented properties, methods & constructors of the Ray class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Ray {
|
||||
pub(crate) origin: Vec3,
|
||||
pub(crate) direction: Vec3,
|
||||
}
|
||||
|
||||
impl Ray {
|
||||
fn closest_point(&self, point: Vec3) -> Vec3 {
|
||||
let norm = self.direction.normalize();
|
||||
let lhs = point - self.origin;
|
||||
|
||||
let dot_product = lhs.dot(norm).max(0.0);
|
||||
self.origin + norm * dot_product
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for Ray {
|
||||
const EXPORT_NAME: &'static str = "Ray";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let ray_new =
|
||||
|_, (origin, direction): (LuaUserDataRef<Vector3>, LuaUserDataRef<Vector3>)| {
|
||||
Ok(Ray {
|
||||
origin: origin.0,
|
||||
direction: direction.0,
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", ray_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Ray {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Origin", |_, this| Ok(Vector3(this.origin)));
|
||||
fields.add_field_method_get("Direction", |_, this| Ok(Vector3(this.direction)));
|
||||
fields.add_field_method_get("Unit", |_, this| {
|
||||
Ok(Ray {
|
||||
origin: this.origin,
|
||||
direction: this.direction.normalize(),
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("ClosestPoint", |_, this, to: LuaUserDataRef<Vector3>| {
|
||||
Ok(Vector3(this.closest_point(to.0)))
|
||||
});
|
||||
methods.add_method("Distance", |_, this, to: LuaUserDataRef<Vector3>| {
|
||||
let closest = this.closest_point(to.0);
|
||||
Ok((closest - to.0).length())
|
||||
});
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ray {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", Vector3(self.origin), Vector3(self.direction))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomRay> for Ray {
|
||||
fn from(v: DomRay) -> Self {
|
||||
Ray {
|
||||
origin: Vector3::from(v.origin).0,
|
||||
direction: Vector3::from(v.direction).0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ray> for DomRay {
|
||||
fn from(v: Ray) -> Self {
|
||||
DomRay {
|
||||
origin: Vector3(v.origin).into(),
|
||||
direction: Vector3(v.direction).into(),
|
||||
}
|
||||
}
|
||||
}
|
129
crates/lune-roblox/src/datatypes/types/rect.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
use glam::Vec2;
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Rect as DomRect;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Vector2};
|
||||
|
||||
/**
|
||||
An implementation of the [Rect](https://create.roblox.com/docs/reference/engine/datatypes/Rect)
|
||||
Roblox datatype, backed by [`glam::Vec2`].
|
||||
|
||||
This implements all documented properties, methods & constructors of the Rect class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Rect {
|
||||
pub(crate) min: Vec2,
|
||||
pub(crate) max: Vec2,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
fn new(lhs: Vec2, rhs: Vec2) -> Self {
|
||||
Self {
|
||||
min: lhs.min(rhs),
|
||||
max: lhs.max(rhs),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for Rect {
|
||||
const EXPORT_NAME: &'static str = "Rect";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
type ArgsVector2s<'lua> = (
|
||||
Option<LuaUserDataRef<'lua, Vector2>>,
|
||||
Option<LuaUserDataRef<'lua, Vector2>>,
|
||||
);
|
||||
type ArgsNums = (Option<f32>, Option<f32>, Option<f32>, Option<f32>);
|
||||
|
||||
let rect_new = |lua, args: LuaMultiValue| {
|
||||
if let Ok((min, max)) = ArgsVector2s::from_lua_multi(args.clone(), lua) {
|
||||
Ok(Rect::new(
|
||||
min.map(|m| *m).unwrap_or_default().0,
|
||||
max.map(|m| *m).unwrap_or_default().0,
|
||||
))
|
||||
} else if let Ok((x0, y0, x1, y1)) = ArgsNums::from_lua_multi(args, lua) {
|
||||
let min = Vec2::new(x0.unwrap_or_default(), y0.unwrap_or_default());
|
||||
let max = Vec2::new(x1.unwrap_or_default(), y1.unwrap_or_default());
|
||||
Ok(Rect::new(min, max))
|
||||
} else {
|
||||
// FUTURE: Better error message here using given arg types
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", rect_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Rect {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Min", |_, this| Ok(Vector2(this.min)));
|
||||
fields.add_field_method_get("Max", |_, this| Ok(Vector2(this.max)));
|
||||
fields.add_field_method_get("Width", |_, this| Ok(this.max.x - this.min.x));
|
||||
fields.add_field_method_get("Height", |_, this| Ok(this.max.y - this.min.y));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);
|
||||
methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add);
|
||||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Rect {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", self.min, self.max)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for Rect {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
Rect::new(-self.min, -self.max)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Rect {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Rect::new(self.min + rhs.min, self.max + rhs.max)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Rect {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Rect::new(self.min - rhs.min, self.max - rhs.max)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomRect> for Rect {
|
||||
fn from(v: DomRect) -> Self {
|
||||
Rect {
|
||||
min: Vec2::new(v.min.x, v.min.y),
|
||||
max: Vec2::new(v.max.x, v.max.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rect> for DomRect {
|
||||
fn from(v: Rect) -> Self {
|
||||
DomRect {
|
||||
min: Vector2(v.min).into(),
|
||||
max: Vector2(v.max).into(),
|
||||
}
|
||||
}
|
||||
}
|
86
crates/lune-roblox/src/datatypes/types/region3.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
use core::fmt;
|
||||
|
||||
use glam::{Mat4, Vec3};
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Region3 as DomRegion3;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, CFrame, Vector3};
|
||||
|
||||
/**
|
||||
An implementation of the [Region3](https://create.roblox.com/docs/reference/engine/datatypes/Region3)
|
||||
Roblox datatype, backed by [`glam::Vec3`].
|
||||
|
||||
This implements all documented properties, methods & constructors of the Region3 class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Region3 {
|
||||
pub(crate) min: Vec3,
|
||||
pub(crate) max: Vec3,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for Region3 {
|
||||
const EXPORT_NAME: &'static str = "Region3";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let region3_new = |_, (min, max): (LuaUserDataRef<Vector3>, LuaUserDataRef<Vector3>)| {
|
||||
Ok(Region3 {
|
||||
min: min.0,
|
||||
max: max.0,
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", region3_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Region3 {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("CFrame", |_, this| {
|
||||
Ok(CFrame(Mat4::from_translation(this.min.lerp(this.max, 0.5))))
|
||||
});
|
||||
fields.add_field_method_get("Size", |_, this| Ok(Vector3(this.max - this.min)));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("ExpandToGrid", |_, this, resolution: f32| {
|
||||
Ok(Region3 {
|
||||
min: (this.min / resolution).floor() * resolution,
|
||||
max: (this.max / resolution).ceil() * resolution,
|
||||
})
|
||||
});
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Region3 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", Vector3(self.min), Vector3(self.max))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomRegion3> for Region3 {
|
||||
fn from(v: DomRegion3) -> Self {
|
||||
Region3 {
|
||||
min: Vector3::from(v.min).0,
|
||||
max: Vector3::from(v.max).0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Region3> for DomRegion3 {
|
||||
fn from(v: Region3) -> Self {
|
||||
DomRegion3 {
|
||||
min: Vector3(v.min).into(),
|
||||
max: Vector3(v.max).into(),
|
||||
}
|
||||
}
|
||||
}
|
77
crates/lune-roblox/src/datatypes/types/region3int16.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use core::fmt;
|
||||
|
||||
use glam::IVec3;
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Region3int16 as DomRegion3int16;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, Vector3int16};
|
||||
|
||||
/**
|
||||
An implementation of the [Region3int16](https://create.roblox.com/docs/reference/engine/datatypes/Region3int16)
|
||||
Roblox datatype, backed by [`glam::IVec3`].
|
||||
|
||||
This implements all documented properties, methods & constructors of the Region3int16 class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Region3int16 {
|
||||
pub(crate) min: IVec3,
|
||||
pub(crate) max: IVec3,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for Region3int16 {
|
||||
const EXPORT_NAME: &'static str = "Region3int16";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let region3int16_new =
|
||||
|_, (min, max): (LuaUserDataRef<Vector3int16>, LuaUserDataRef<Vector3int16>)| {
|
||||
Ok(Region3int16 {
|
||||
min: min.0,
|
||||
max: max.0,
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", region3int16_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Region3int16 {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Min", |_, this| Ok(Vector3int16(this.min)));
|
||||
fields.add_field_method_get("Max", |_, this| Ok(Vector3int16(this.max)));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Region3int16 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", Vector3int16(self.min), Vector3int16(self.max))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomRegion3int16> for Region3int16 {
|
||||
fn from(v: DomRegion3int16) -> Self {
|
||||
Region3int16 {
|
||||
min: Vector3int16::from(v.min).0,
|
||||
max: Vector3int16::from(v.max).0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Region3int16> for DomRegion3int16 {
|
||||
fn from(v: Region3int16) -> Self {
|
||||
DomRegion3int16 {
|
||||
min: Vector3int16(v.min).into(),
|
||||
max: Vector3int16(v.max).into(),
|
||||
}
|
||||
}
|
||||
}
|
123
crates/lune-roblox/src/datatypes/types/udim.rs
Normal file
|
@ -0,0 +1,123 @@
|
|||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::UDim as DomUDim;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [UDim](https://create.roblox.com/docs/reference/engine/datatypes/UDim) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `UDim` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct UDim {
|
||||
pub(crate) scale: f32,
|
||||
pub(crate) offset: i32,
|
||||
}
|
||||
|
||||
impl UDim {
|
||||
pub(super) fn new(scale: f32, offset: i32) -> Self {
|
||||
Self { scale, offset }
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for UDim {
|
||||
const EXPORT_NAME: &'static str = "UDim";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let udim_new = |_, (scale, offset): (Option<f32>, Option<i32>)| {
|
||||
Ok(UDim {
|
||||
scale: scale.unwrap_or_default(),
|
||||
offset: offset.unwrap_or_default(),
|
||||
})
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", udim_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for UDim {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Scale", |_, this| Ok(this.scale));
|
||||
fields.add_field_method_get("Offset", |_, this| Ok(this.offset));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);
|
||||
methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add);
|
||||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UDim {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale: 0f32,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UDim {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", self.scale, self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for UDim {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
UDim {
|
||||
scale: -self.scale,
|
||||
offset: -self.offset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for UDim {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
UDim {
|
||||
scale: self.scale + rhs.scale,
|
||||
offset: self.offset + rhs.offset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for UDim {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
UDim {
|
||||
scale: self.scale - rhs.scale,
|
||||
offset: self.offset - rhs.offset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomUDim> for UDim {
|
||||
fn from(v: DomUDim) -> Self {
|
||||
UDim {
|
||||
scale: v.scale,
|
||||
offset: v.offset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UDim> for DomUDim {
|
||||
fn from(v: UDim) -> Self {
|
||||
DomUDim {
|
||||
scale: v.scale,
|
||||
offset: v.offset,
|
||||
}
|
||||
}
|
||||
}
|
172
crates/lune-roblox/src/datatypes/types/udim2.rs
Normal file
|
@ -0,0 +1,172 @@
|
|||
#![allow(clippy::items_after_statements)]
|
||||
|
||||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
use glam::Vec2;
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::UDim2 as DomUDim2;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::{super::*, UDim};
|
||||
|
||||
/**
|
||||
An implementation of the [UDim2](https://create.roblox.com/docs/reference/engine/datatypes/UDim2) Roblox datatype.
|
||||
|
||||
This implements all documented properties, methods & constructors of the `UDim2` class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct UDim2 {
|
||||
pub(crate) x: UDim,
|
||||
pub(crate) y: UDim,
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for UDim2 {
|
||||
const EXPORT_NAME: &'static str = "UDim2";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let udim2_from_offset = |_, (x, y): (Option<i32>, Option<i32>)| {
|
||||
Ok(UDim2 {
|
||||
x: UDim::new(0f32, x.unwrap_or_default()),
|
||||
y: UDim::new(0f32, y.unwrap_or_default()),
|
||||
})
|
||||
};
|
||||
|
||||
let udim2_from_scale = |_, (x, y): (Option<f32>, Option<f32>)| {
|
||||
Ok(UDim2 {
|
||||
x: UDim::new(x.unwrap_or_default(), 0),
|
||||
y: UDim::new(y.unwrap_or_default(), 0),
|
||||
})
|
||||
};
|
||||
|
||||
type ArgsUDims<'lua> = (
|
||||
Option<LuaUserDataRef<'lua, UDim>>,
|
||||
Option<LuaUserDataRef<'lua, UDim>>,
|
||||
);
|
||||
type ArgsNums = (Option<f32>, Option<i32>, Option<f32>, Option<i32>);
|
||||
let udim2_new = |lua, args: LuaMultiValue| {
|
||||
if let Ok((x, y)) = ArgsUDims::from_lua_multi(args.clone(), lua) {
|
||||
Ok(UDim2 {
|
||||
x: x.map(|x| *x).unwrap_or_default(),
|
||||
y: y.map(|y| *y).unwrap_or_default(),
|
||||
})
|
||||
} else if let Ok((sx, ox, sy, oy)) = ArgsNums::from_lua_multi(args, lua) {
|
||||
Ok(UDim2 {
|
||||
x: UDim::new(sx.unwrap_or_default(), ox.unwrap_or_default()),
|
||||
y: UDim::new(sy.unwrap_or_default(), oy.unwrap_or_default()),
|
||||
})
|
||||
} else {
|
||||
// FUTURE: Better error message here using given arg types
|
||||
Err(LuaError::RuntimeError(
|
||||
"Invalid arguments to constructor".to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("fromOffset", udim2_from_offset)?
|
||||
.with_function("fromScale", udim2_from_scale)?
|
||||
.with_function("new", udim2_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for UDim2 {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("X", |_, this| Ok(this.x));
|
||||
fields.add_field_method_get("Y", |_, this| Ok(this.y));
|
||||
fields.add_field_method_get("Width", |_, this| Ok(this.x));
|
||||
fields.add_field_method_get("Height", |_, this| Ok(this.y));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method(
|
||||
"Lerp",
|
||||
|_, this, (goal, alpha): (LuaUserDataRef<UDim2>, f32)| {
|
||||
let this_x = Vec2::new(this.x.scale, this.x.offset as f32);
|
||||
let goal_x = Vec2::new(goal.x.scale, goal.x.offset as f32);
|
||||
|
||||
let this_y = Vec2::new(this.y.scale, this.y.offset as f32);
|
||||
let goal_y = Vec2::new(goal.y.scale, goal.y.offset as f32);
|
||||
|
||||
let x = this_x.lerp(goal_x, alpha);
|
||||
let y = this_y.lerp(goal_y, alpha);
|
||||
|
||||
Ok(UDim2 {
|
||||
x: UDim {
|
||||
scale: x.x,
|
||||
offset: x.y.clamp(i32::MIN as f32, i32::MAX as f32).round() as i32,
|
||||
},
|
||||
y: UDim {
|
||||
scale: y.x,
|
||||
offset: y.y.clamp(i32::MIN as f32, i32::MAX as f32).round() as i32,
|
||||
},
|
||||
})
|
||||
},
|
||||
);
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);
|
||||
methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add);
|
||||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for UDim2 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", self.x, self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for UDim2 {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
UDim2 {
|
||||
x: -self.x,
|
||||
y: -self.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for UDim2 {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
UDim2 {
|
||||
x: self.x + rhs.x,
|
||||
y: self.y + rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for UDim2 {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
UDim2 {
|
||||
x: self.x - rhs.x,
|
||||
y: self.y - rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomUDim2> for UDim2 {
|
||||
fn from(v: DomUDim2) -> Self {
|
||||
UDim2 {
|
||||
x: v.x.into(),
|
||||
y: v.y.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UDim2> for DomUDim2 {
|
||||
fn from(v: UDim2) -> Self {
|
||||
DomUDim2 {
|
||||
x: v.x.into(),
|
||||
y: v.y.into(),
|
||||
}
|
||||
}
|
||||
}
|
181
crates/lune-roblox/src/datatypes/types/vector2.rs
Normal file
|
@ -0,0 +1,181 @@
|
|||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
use glam::{Vec2, Vec3};
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Vector2 as DomVector2;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [Vector2](https://create.roblox.com/docs/reference/engine/datatypes/Vector2)
|
||||
Roblox datatype, backed by [`glam::Vec2`].
|
||||
|
||||
This implements all documented properties, methods &
|
||||
constructors of the Vector2 class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Vector2(pub Vec2);
|
||||
|
||||
impl LuaExportsTable<'_> for Vector2 {
|
||||
const EXPORT_NAME: &'static str = "Vector2";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let vector2_new = |_, (x, y): (Option<f32>, Option<f32>)| {
|
||||
Ok(Vector2(Vec2 {
|
||||
x: x.unwrap_or_default(),
|
||||
y: y.unwrap_or_default(),
|
||||
}))
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("xAxis", Vector2(Vec2::X))?
|
||||
.with_value("yAxis", Vector2(Vec2::Y))?
|
||||
.with_value("zero", Vector2(Vec2::ZERO))?
|
||||
.with_value("one", Vector2(Vec2::ONE))?
|
||||
.with_function("new", vector2_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Vector2 {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Magnitude", |_, this| Ok(this.0.length()));
|
||||
fields.add_field_method_get("Unit", |_, this| Ok(Vector2(this.0.normalize())));
|
||||
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
|
||||
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("Angle", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
||||
Ok(this.0.angle_between(rhs.0))
|
||||
});
|
||||
methods.add_method("Cross", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
||||
let this_v3 = Vec3::new(this.0.x, this.0.y, 0f32);
|
||||
let rhs_v3 = Vec3::new(rhs.0.x, rhs.0.y, 0f32);
|
||||
Ok(this_v3.cross(rhs_v3).z)
|
||||
});
|
||||
methods.add_method("Dot", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
||||
Ok(this.0.dot(rhs.0))
|
||||
});
|
||||
methods.add_method(
|
||||
"FuzzyEq",
|
||||
|_, this, (rhs, epsilon): (LuaUserDataRef<Vector2>, f32)| {
|
||||
let eq_x = (rhs.0.x - this.0.x).abs() <= epsilon;
|
||||
let eq_y = (rhs.0.y - this.0.y).abs() <= epsilon;
|
||||
Ok(eq_x && eq_y)
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"Lerp",
|
||||
|_, this, (rhs, alpha): (LuaUserDataRef<Vector2>, f32)| {
|
||||
Ok(Vector2(this.0.lerp(rhs.0, alpha)))
|
||||
},
|
||||
);
|
||||
methods.add_method("Max", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
||||
Ok(Vector2(this.0.max(rhs.0)))
|
||||
});
|
||||
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector2>| {
|
||||
Ok(Vector2(this.0.min(rhs.0)))
|
||||
});
|
||||
methods.add_method("Abs", |_, this, ()| Ok(Vector2(this.0.abs())));
|
||||
methods.add_method("Ceil", |_, this, ()| Ok(Vector2(this.0.ceil())));
|
||||
methods.add_method("Floor", |_, this, ()| Ok(Vector2(this.0.floor())));
|
||||
methods.add_method("Sign", |_, this, ()| Ok(Vector2(this.0.signum())));
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);
|
||||
methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add);
|
||||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_f32);
|
||||
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_f32);
|
||||
methods.add_meta_method(LuaMetaMethod::IDiv, userdata_impl_idiv_f32);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Vector2 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", self.0.x, self.0.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for Vector2 {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
Vector2(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Vector2 {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Vector2(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Vector2 {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Vector2(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for Vector2 {
|
||||
type Output = Vector2;
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<f32> for Vector2 {
|
||||
type Output = Vector2;
|
||||
fn mul(self, rhs: f32) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for Vector2 {
|
||||
type Output = Vector2;
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 / rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<f32> for Vector2 {
|
||||
type Output = Vector2;
|
||||
fn div(self, rhs: f32) -> Self::Output {
|
||||
Self(self.0 / rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl IDiv for Vector2 {
|
||||
type Output = Vector2;
|
||||
fn idiv(self, rhs: Self) -> Self::Output {
|
||||
Self((self.0 / rhs.0).floor())
|
||||
}
|
||||
}
|
||||
|
||||
impl IDiv<f32> for Vector2 {
|
||||
type Output = Vector2;
|
||||
fn idiv(self, rhs: f32) -> Self::Output {
|
||||
Self((self.0 / rhs).floor())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomVector2> for Vector2 {
|
||||
fn from(v: DomVector2) -> Self {
|
||||
Vector2(Vec2 { x: v.x, y: v.y })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector2> for DomVector2 {
|
||||
fn from(v: Vector2) -> Self {
|
||||
DomVector2 { x: v.0.x, y: v.0.y }
|
||||
}
|
||||
}
|
129
crates/lune-roblox/src/datatypes/types/vector2int16.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
use glam::IVec2;
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Vector2int16 as DomVector2int16;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [Vector2int16](https://create.roblox.com/docs/reference/engine/datatypes/Vector2int16)
|
||||
Roblox datatype, backed by [`glam::IVec2`].
|
||||
|
||||
This implements all documented properties, methods &
|
||||
constructors of the Vector2int16 class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Vector2int16(pub IVec2);
|
||||
|
||||
impl LuaExportsTable<'_> for Vector2int16 {
|
||||
const EXPORT_NAME: &'static str = "Vector2int16";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let vector2int16_new = |_, (x, y): (Option<i16>, Option<i16>)| {
|
||||
Ok(Vector2int16(IVec2 {
|
||||
x: x.unwrap_or_default() as i32,
|
||||
y: y.unwrap_or_default() as i32,
|
||||
}))
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", vector2int16_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Vector2int16 {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
|
||||
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);
|
||||
methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add);
|
||||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_i32);
|
||||
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_i32);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Vector2int16 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", self.0.x, self.0.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for Vector2int16 {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
Vector2int16(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Vector2int16 {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Vector2int16(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Vector2int16 {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Vector2int16(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for Vector2int16 {
|
||||
type Output = Vector2int16;
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<i32> for Vector2int16 {
|
||||
type Output = Vector2int16;
|
||||
fn mul(self, rhs: i32) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for Vector2int16 {
|
||||
type Output = Vector2int16;
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 / rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<i32> for Vector2int16 {
|
||||
type Output = Vector2int16;
|
||||
fn div(self, rhs: i32) -> Self::Output {
|
||||
Self(self.0 / rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomVector2int16> for Vector2int16 {
|
||||
fn from(v: DomVector2int16) -> Self {
|
||||
Vector2int16(IVec2 {
|
||||
x: v.x.clamp(i16::MIN, i16::MAX) as i32,
|
||||
y: v.y.clamp(i16::MIN, i16::MAX) as i32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector2int16> for DomVector2int16 {
|
||||
fn from(v: Vector2int16) -> Self {
|
||||
DomVector2int16 {
|
||||
x: v.0.x.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
||||
y: v.0.y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
||||
}
|
||||
}
|
||||
}
|
239
crates/lune-roblox/src/datatypes/types/vector3.rs
Normal file
|
@ -0,0 +1,239 @@
|
|||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
use glam::Vec3;
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Vector3 as DomVector3;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::{datatypes::util::round_float_decimal, exports::LuaExportsTable};
|
||||
|
||||
use super::{super::*, EnumItem};
|
||||
|
||||
/**
|
||||
An implementation of the [Vector3](https://create.roblox.com/docs/reference/engine/datatypes/Vector3)
|
||||
Roblox datatype, backed by [`glam::Vec3`].
|
||||
|
||||
This implements all documented properties, methods &
|
||||
constructors of the Vector3 class as of March 2023.
|
||||
|
||||
Note that this does not use native Luau vectors to simplify implementation
|
||||
and instead allow us to implement all abovementioned APIs accurately.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Vector3(pub Vec3);
|
||||
|
||||
impl LuaExportsTable<'_> for Vector3 {
|
||||
const EXPORT_NAME: &'static str = "Vector3";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let vector3_from_axis = |_, normal_id: LuaUserDataRef<EnumItem>| {
|
||||
if normal_id.parent.desc.name == "Axis" {
|
||||
Ok(match normal_id.name.as_str() {
|
||||
"X" => Vector3(Vec3::X),
|
||||
"Y" => Vector3(Vec3::Y),
|
||||
"Z" => Vector3(Vec3::Z),
|
||||
name => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Axis '{name}' is not known",
|
||||
)))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"EnumItem must be a Axis, got {}",
|
||||
normal_id.parent.desc.name
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let vector3_from_normal_id = |_, normal_id: LuaUserDataRef<EnumItem>| {
|
||||
if normal_id.parent.desc.name == "NormalId" {
|
||||
Ok(match normal_id.name.as_str() {
|
||||
"Left" => Vector3(Vec3::X),
|
||||
"Top" => Vector3(Vec3::Y),
|
||||
"Front" => Vector3(-Vec3::Z),
|
||||
"Right" => Vector3(-Vec3::X),
|
||||
"Bottom" => Vector3(-Vec3::Y),
|
||||
"Back" => Vector3(Vec3::Z),
|
||||
name => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"NormalId '{name}' is not known",
|
||||
)))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"EnumItem must be a NormalId, got {}",
|
||||
normal_id.parent.desc.name
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let vector3_new = |_, (x, y, z): (Option<f32>, Option<f32>, Option<f32>)| {
|
||||
Ok(Vector3(Vec3 {
|
||||
x: x.unwrap_or_default(),
|
||||
y: y.unwrap_or_default(),
|
||||
z: z.unwrap_or_default(),
|
||||
}))
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("xAxis", Vector3(Vec3::X))?
|
||||
.with_value("yAxis", Vector3(Vec3::Y))?
|
||||
.with_value("zAxis", Vector3(Vec3::Z))?
|
||||
.with_value("zero", Vector3(Vec3::ZERO))?
|
||||
.with_value("one", Vector3(Vec3::ONE))?
|
||||
.with_function("fromAxis", vector3_from_axis)?
|
||||
.with_function("fromNormalId", vector3_from_normal_id)?
|
||||
.with_function("new", vector3_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Vector3 {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Magnitude", |_, this| Ok(this.0.length()));
|
||||
fields.add_field_method_get("Unit", |_, this| Ok(Vector3(this.0.normalize())));
|
||||
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
|
||||
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
|
||||
fields.add_field_method_get("Z", |_, this| Ok(this.0.z));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Methods
|
||||
methods.add_method("Angle", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
||||
Ok(this.0.angle_between(rhs.0))
|
||||
});
|
||||
methods.add_method("Cross", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
||||
Ok(Vector3(this.0.cross(rhs.0)))
|
||||
});
|
||||
methods.add_method("Dot", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
||||
Ok(this.0.dot(rhs.0))
|
||||
});
|
||||
methods.add_method(
|
||||
"FuzzyEq",
|
||||
|_, this, (rhs, epsilon): (LuaUserDataRef<Vector3>, f32)| {
|
||||
let eq_x = (rhs.0.x - this.0.x).abs() <= epsilon;
|
||||
let eq_y = (rhs.0.y - this.0.y).abs() <= epsilon;
|
||||
let eq_z = (rhs.0.z - this.0.z).abs() <= epsilon;
|
||||
Ok(eq_x && eq_y && eq_z)
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"Lerp",
|
||||
|_, this, (rhs, alpha): (LuaUserDataRef<Vector3>, f32)| {
|
||||
Ok(Vector3(this.0.lerp(rhs.0, alpha)))
|
||||
},
|
||||
);
|
||||
methods.add_method("Max", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
||||
Ok(Vector3(this.0.max(rhs.0)))
|
||||
});
|
||||
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector3>| {
|
||||
Ok(Vector3(this.0.min(rhs.0)))
|
||||
});
|
||||
methods.add_method("Abs", |_, this, ()| Ok(Vector3(this.0.abs())));
|
||||
methods.add_method("Ceil", |_, this, ()| Ok(Vector3(this.0.ceil())));
|
||||
methods.add_method("Floor", |_, this, ()| Ok(Vector3(this.0.floor())));
|
||||
methods.add_method("Sign", |_, this, ()| Ok(Vector3(this.0.signum())));
|
||||
// Metamethods
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);
|
||||
methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add);
|
||||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_f32);
|
||||
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_f32);
|
||||
methods.add_meta_method(LuaMetaMethod::IDiv, userdata_impl_idiv_f32);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Vector3 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}, {}", self.0.x, self.0.y, self.0.z)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for Vector3 {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
Vector3(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Vector3 {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Vector3(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Vector3 {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Vector3(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for Vector3 {
|
||||
type Output = Vector3;
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<f32> for Vector3 {
|
||||
type Output = Vector3;
|
||||
fn mul(self, rhs: f32) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for Vector3 {
|
||||
type Output = Vector3;
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 / rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<f32> for Vector3 {
|
||||
type Output = Vector3;
|
||||
fn div(self, rhs: f32) -> Self::Output {
|
||||
Self(self.0 / rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl IDiv for Vector3 {
|
||||
type Output = Vector3;
|
||||
fn idiv(self, rhs: Self) -> Self::Output {
|
||||
Self((self.0 / rhs.0).floor())
|
||||
}
|
||||
}
|
||||
|
||||
impl IDiv<f32> for Vector3 {
|
||||
type Output = Vector3;
|
||||
fn idiv(self, rhs: f32) -> Self::Output {
|
||||
Self((self.0 / rhs).floor())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomVector3> for Vector3 {
|
||||
fn from(v: DomVector3) -> Self {
|
||||
Vector3(Vec3 {
|
||||
x: v.x,
|
||||
y: v.y,
|
||||
z: v.z,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector3> for DomVector3 {
|
||||
fn from(v: Vector3) -> Self {
|
||||
DomVector3 {
|
||||
x: round_float_decimal(v.0.x),
|
||||
y: round_float_decimal(v.0.y),
|
||||
z: round_float_decimal(v.0.z),
|
||||
}
|
||||
}
|
||||
}
|
133
crates/lune-roblox/src/datatypes/types/vector3int16.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
use core::fmt;
|
||||
use std::ops;
|
||||
|
||||
use glam::IVec3;
|
||||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::Vector3int16 as DomVector3int16;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::exports::LuaExportsTable;
|
||||
|
||||
use super::super::*;
|
||||
|
||||
/**
|
||||
An implementation of the [Vector3int16](https://create.roblox.com/docs/reference/engine/datatypes/Vector3int16)
|
||||
Roblox datatype, backed by [`glam::IVec3`].
|
||||
|
||||
This implements all documented properties, methods &
|
||||
constructors of the Vector3int16 class as of March 2023.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Vector3int16(pub IVec3);
|
||||
|
||||
impl LuaExportsTable<'_> for Vector3int16 {
|
||||
const EXPORT_NAME: &'static str = "Vector3int16";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let vector3int16_new = |_, (x, y, z): (Option<i16>, Option<i16>, Option<i16>)| {
|
||||
Ok(Vector3int16(IVec3 {
|
||||
x: x.unwrap_or_default() as i32,
|
||||
y: y.unwrap_or_default() as i32,
|
||||
z: z.unwrap_or_default() as i32,
|
||||
}))
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", vector3int16_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Vector3int16 {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("X", |_, this| Ok(this.0.x));
|
||||
fields.add_field_method_get("Y", |_, this| Ok(this.0.y));
|
||||
fields.add_field_method_get("Z", |_, this| Ok(this.0.z));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_meta_method(LuaMetaMethod::Unm, userdata_impl_unm);
|
||||
methods.add_meta_method(LuaMetaMethod::Add, userdata_impl_add);
|
||||
methods.add_meta_method(LuaMetaMethod::Sub, userdata_impl_sub);
|
||||
methods.add_meta_method(LuaMetaMethod::Mul, userdata_impl_mul_i32);
|
||||
methods.add_meta_method(LuaMetaMethod::Div, userdata_impl_div_i32);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Vector3int16 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}, {}", self.0.x, self.0.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for Vector3int16 {
|
||||
type Output = Self;
|
||||
fn neg(self) -> Self::Output {
|
||||
Vector3int16(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Vector3int16 {
|
||||
type Output = Self;
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Vector3int16(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Vector3int16 {
|
||||
type Output = Self;
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Vector3int16(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul for Vector3int16 {
|
||||
type Output = Vector3int16;
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<i32> for Vector3int16 {
|
||||
type Output = Vector3int16;
|
||||
fn mul(self, rhs: i32) -> Self::Output {
|
||||
Self(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for Vector3int16 {
|
||||
type Output = Vector3int16;
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 / rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<i32> for Vector3int16 {
|
||||
type Output = Vector3int16;
|
||||
fn div(self, rhs: i32) -> Self::Output {
|
||||
Self(self.0 / rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomVector3int16> for Vector3int16 {
|
||||
fn from(v: DomVector3int16) -> Self {
|
||||
Vector3int16(IVec3 {
|
||||
x: v.x.clamp(i16::MIN, i16::MAX) as i32,
|
||||
y: v.y.clamp(i16::MIN, i16::MAX) as i32,
|
||||
z: v.z.clamp(i16::MIN, i16::MAX) as i32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vector3int16> for DomVector3int16 {
|
||||
fn from(v: Vector3int16) -> Self {
|
||||
DomVector3int16 {
|
||||
x: v.0.x.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
||||
y: v.0.y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
||||
z: v.0.z.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
|
||||
}
|
||||
}
|
||||
}
|
16
crates/lune-roblox/src/datatypes/util.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
// HACK: We round to the nearest Very Small Decimal
|
||||
// to reduce writing out floating point accumulation
|
||||
// errors to files (mostly relevant for xml formats)
|
||||
const ROUNDING: usize = 65_536; // 2 ^ 16
|
||||
|
||||
pub fn round_float_decimal(value: f32) -> f32 {
|
||||
let place = ROUNDING as f32;
|
||||
|
||||
// Round only the fractional part, we do not want to
|
||||
// lose any float precision in case a user for some
|
||||
// reason has very very large float numbers in files
|
||||
let whole = value.trunc();
|
||||
let fract = (value.fract() * place).round() / place;
|
||||
|
||||
whole + fract
|
||||
}
|
28
crates/lune-roblox/src/document/error.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use mlua::prelude::*;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum DocumentError {
|
||||
#[error("Unknown document kind")]
|
||||
UnknownKind,
|
||||
#[error("Unknown document format")]
|
||||
UnknownFormat,
|
||||
#[error("Failed to read document from buffer - {0}")]
|
||||
ReadError(String),
|
||||
#[error("Failed to write document to buffer - {0}")]
|
||||
WriteError(String),
|
||||
#[error("Failed to convert into a DataModel - the given document is not a place")]
|
||||
IntoDataModelInvalidArgs,
|
||||
#[error("Failed to convert into array of Instances - the given document is a model")]
|
||||
IntoInstanceArrayInvalidArgs,
|
||||
#[error("Failed to convert into a place - the given instance is not a DataModel")]
|
||||
FromDataModelInvalidArgs,
|
||||
#[error("Failed to convert into a model - a given instance is a DataModel")]
|
||||
FromInstanceArrayInvalidArgs,
|
||||
}
|
||||
|
||||
impl From<DocumentError> for LuaError {
|
||||
fn from(value: DocumentError) -> Self {
|
||||
Self::RuntimeError(value.to_string())
|
||||
}
|
||||
}
|
202
crates/lune-roblox/src/document/format.rs
Normal file
|
@ -0,0 +1,202 @@
|
|||
// Original implementation from Remodel:
|
||||
// https://github.com/rojo-rbx/remodel/blob/master/src/sniff_type.rs
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
/**
|
||||
A document format specifier.
|
||||
|
||||
Valid variants are the following:
|
||||
|
||||
- `Binary`
|
||||
- `Xml`
|
||||
|
||||
Other variants are only to be used for logic internal to this crate.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum DocumentFormat {
|
||||
Binary,
|
||||
Xml,
|
||||
}
|
||||
|
||||
impl DocumentFormat {
|
||||
/**
|
||||
Try to convert a file extension into a valid document format specifier.
|
||||
|
||||
Returns `None` if the file extension is not a canonical roblox file format extension.
|
||||
*/
|
||||
pub fn from_extension(extension: impl AsRef<str>) -> Option<Self> {
|
||||
match extension.as_ref() {
|
||||
"rbxl" | "rbxm" => Some(Self::Binary),
|
||||
"rbxlx" | "rbxmx" => Some(Self::Xml),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Try to convert a file path into a valid document format specifier.
|
||||
|
||||
Returns `None` if the file extension of the path
|
||||
is not a canonical roblox file format extension.
|
||||
*/
|
||||
pub fn from_path(path: impl AsRef<Path>) -> Option<Self> {
|
||||
match path
|
||||
.as_ref()
|
||||
.extension()
|
||||
.map(|ext| ext.to_string_lossy())
|
||||
.as_deref()
|
||||
{
|
||||
Some("rbxl") | Some("rbxm") => Some(Self::Binary),
|
||||
Some("rbxlx") | Some("rbxmx") => Some(Self::Xml),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Try to detect a document format specifier from file contents.
|
||||
|
||||
Returns `None` if the file contents do not seem to be from a valid roblox file.
|
||||
*/
|
||||
pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Option<Self> {
|
||||
let header = bytes.as_ref().get(0..8)?;
|
||||
|
||||
if header.starts_with(b"<roblox") {
|
||||
match header[7] {
|
||||
b'!' => Some(Self::Binary),
|
||||
b' ' | b'>' => Some(Self::Xml),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DocumentFormat {
|
||||
fn default() -> Self {
|
||||
Self::Binary
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_extension_binary() {
|
||||
assert_eq!(
|
||||
DocumentFormat::from_extension("rbxl"),
|
||||
Some(DocumentFormat::Binary)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentFormat::from_extension("rbxm"),
|
||||
Some(DocumentFormat::Binary)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_extension_xml() {
|
||||
assert_eq!(
|
||||
DocumentFormat::from_extension("rbxlx"),
|
||||
Some(DocumentFormat::Xml)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentFormat::from_extension("rbxmx"),
|
||||
Some(DocumentFormat::Xml)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_extension_invalid() {
|
||||
assert_eq!(DocumentFormat::from_extension("csv"), None);
|
||||
assert_eq!(DocumentFormat::from_extension("json"), None);
|
||||
assert_eq!(DocumentFormat::from_extension("rbx"), None);
|
||||
assert_eq!(DocumentFormat::from_extension("rbxn"), None);
|
||||
assert_eq!(DocumentFormat::from_extension("xlx"), None);
|
||||
assert_eq!(DocumentFormat::from_extension("xmx"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_binary() {
|
||||
assert_eq!(
|
||||
DocumentFormat::from_path(PathBuf::from("model.rbxl")),
|
||||
Some(DocumentFormat::Binary)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentFormat::from_path(PathBuf::from("model.rbxm")),
|
||||
Some(DocumentFormat::Binary)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_xml() {
|
||||
assert_eq!(
|
||||
DocumentFormat::from_path(PathBuf::from("place.rbxlx")),
|
||||
Some(DocumentFormat::Xml)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentFormat::from_path(PathBuf::from("place.rbxmx")),
|
||||
Some(DocumentFormat::Xml)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_invalid() {
|
||||
assert_eq!(
|
||||
DocumentFormat::from_path(PathBuf::from("data-file.csv")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentFormat::from_path(PathBuf::from("nested/path/file.json")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentFormat::from_path(PathBuf::from(".no-name-strange-rbx")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentFormat::from_path(PathBuf::from("file_without_extension")),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_bytes_binary() {
|
||||
assert_eq!(
|
||||
DocumentFormat::from_bytes(b"<roblox!hello"),
|
||||
Some(DocumentFormat::Binary)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentFormat::from_bytes(b"<roblox!"),
|
||||
Some(DocumentFormat::Binary)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_bytes_xml() {
|
||||
assert_eq!(
|
||||
DocumentFormat::from_bytes(b"<roblox xml:someschemajunk>"),
|
||||
Some(DocumentFormat::Xml)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentFormat::from_bytes(b"<roblox>"),
|
||||
Some(DocumentFormat::Xml)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_bytes_invalid() {
|
||||
assert_eq!(DocumentFormat::from_bytes(b""), None);
|
||||
assert_eq!(DocumentFormat::from_bytes(b" roblox"), None);
|
||||
assert_eq!(DocumentFormat::from_bytes(b"<roblox"), None);
|
||||
assert_eq!(DocumentFormat::from_bytes(b"<roblox-"), None);
|
||||
}
|
||||
}
|
209
crates/lune-roblox/src/document/kind.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
use std::path::Path;
|
||||
|
||||
use rbx_dom_weak::WeakDom;
|
||||
|
||||
use crate::shared::instance::class_is_a_service;
|
||||
|
||||
/**
|
||||
A document kind specifier.
|
||||
|
||||
Valid variants are the following:
|
||||
|
||||
- `Model`
|
||||
- `Place`
|
||||
|
||||
Other variants are only to be used for logic internal to this crate.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum DocumentKind {
|
||||
Place,
|
||||
Model,
|
||||
}
|
||||
|
||||
impl DocumentKind {
|
||||
/**
|
||||
Try to convert a file extension into a valid document kind specifier.
|
||||
|
||||
Returns `None` if the file extension is not a canonical roblox file format extension.
|
||||
*/
|
||||
pub fn from_extension(extension: impl AsRef<str>) -> Option<Self> {
|
||||
match extension.as_ref() {
|
||||
"rbxl" | "rbxlx" => Some(Self::Place),
|
||||
"rbxm" | "rbxmx" => Some(Self::Model),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Try to convert a file path into a valid document kind specifier.
|
||||
|
||||
Returns `None` if the file extension of the path
|
||||
is not a canonical roblox file format extension.
|
||||
*/
|
||||
pub fn from_path(path: impl AsRef<Path>) -> Option<Self> {
|
||||
match path
|
||||
.as_ref()
|
||||
.extension()
|
||||
.map(|ext| ext.to_string_lossy())
|
||||
.as_deref()
|
||||
{
|
||||
Some("rbxl") | Some("rbxlx") => Some(Self::Place),
|
||||
Some("rbxm") | Some("rbxmx") => Some(Self::Model),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Try to detect a document kind specifier from a weak dom.
|
||||
|
||||
Returns `None` if the given dom is empty and as such can not have its kind inferred.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn from_weak_dom(dom: &WeakDom) -> Option<Self> {
|
||||
let mut has_top_level_child = false;
|
||||
let mut has_top_level_service = false;
|
||||
for child_ref in dom.root().children() {
|
||||
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
|
||||
has_top_level_child = true;
|
||||
if class_is_a_service(child_inst.class).unwrap_or(false) {
|
||||
has_top_level_service = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if has_top_level_service {
|
||||
Some(Self::Place)
|
||||
} else if has_top_level_child {
|
||||
Some(Self::Model)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rbx_dom_weak::InstanceBuilder;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_extension_place() {
|
||||
assert_eq!(
|
||||
DocumentKind::from_extension("rbxl"),
|
||||
Some(DocumentKind::Place)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentKind::from_extension("rbxlx"),
|
||||
Some(DocumentKind::Place)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_extension_model() {
|
||||
assert_eq!(
|
||||
DocumentKind::from_extension("rbxm"),
|
||||
Some(DocumentKind::Model)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentKind::from_extension("rbxmx"),
|
||||
Some(DocumentKind::Model)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_extension_invalid() {
|
||||
assert_eq!(DocumentKind::from_extension("csv"), None);
|
||||
assert_eq!(DocumentKind::from_extension("json"), None);
|
||||
assert_eq!(DocumentKind::from_extension("rbx"), None);
|
||||
assert_eq!(DocumentKind::from_extension("rbxn"), None);
|
||||
assert_eq!(DocumentKind::from_extension("xlx"), None);
|
||||
assert_eq!(DocumentKind::from_extension("xmx"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_place() {
|
||||
assert_eq!(
|
||||
DocumentKind::from_path(PathBuf::from("place.rbxl")),
|
||||
Some(DocumentKind::Place)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentKind::from_path(PathBuf::from("place.rbxlx")),
|
||||
Some(DocumentKind::Place)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_model() {
|
||||
assert_eq!(
|
||||
DocumentKind::from_path(PathBuf::from("model.rbxm")),
|
||||
Some(DocumentKind::Model)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DocumentKind::from_path(PathBuf::from("model.rbxmx")),
|
||||
Some(DocumentKind::Model)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_invalid() {
|
||||
assert_eq!(
|
||||
DocumentKind::from_path(PathBuf::from("data-file.csv")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentKind::from_path(PathBuf::from("nested/path/file.json")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentKind::from_path(PathBuf::from(".no-name-strange-rbx")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentKind::from_path(PathBuf::from("file_without_extension")),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_weak_dom() {
|
||||
let empty = WeakDom::new(InstanceBuilder::new("Instance"));
|
||||
assert_eq!(DocumentKind::from_weak_dom(&empty), None);
|
||||
|
||||
let with_services = WeakDom::new(
|
||||
InstanceBuilder::new("Instance")
|
||||
.with_child(InstanceBuilder::new("Workspace"))
|
||||
.with_child(InstanceBuilder::new("ReplicatedStorage")),
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentKind::from_weak_dom(&with_services),
|
||||
Some(DocumentKind::Place)
|
||||
);
|
||||
|
||||
let with_children = WeakDom::new(
|
||||
InstanceBuilder::new("Instance")
|
||||
.with_child(InstanceBuilder::new("Model"))
|
||||
.with_child(InstanceBuilder::new("Part")),
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentKind::from_weak_dom(&with_children),
|
||||
Some(DocumentKind::Model)
|
||||
);
|
||||
|
||||
let with_mixed = WeakDom::new(
|
||||
InstanceBuilder::new("Instance")
|
||||
.with_child(InstanceBuilder::new("Workspace"))
|
||||
.with_child(InstanceBuilder::new("Part")),
|
||||
);
|
||||
assert_eq!(
|
||||
DocumentKind::from_weak_dom(&with_mixed),
|
||||
Some(DocumentKind::Place)
|
||||
);
|
||||
}
|
||||
}
|
318
crates/lune-roblox/src/document/mod.rs
Normal file
|
@ -0,0 +1,318 @@
|
|||
use rbx_dom_weak::{types::Ref as DomRef, InstanceBuilder as DomInstanceBuilder, WeakDom};
|
||||
use rbx_xml::{
|
||||
DecodeOptions as XmlDecodeOptions, DecodePropertyBehavior as XmlDecodePropertyBehavior,
|
||||
EncodeOptions as XmlEncodeOptions, EncodePropertyBehavior as XmlEncodePropertyBehavior,
|
||||
};
|
||||
|
||||
mod error;
|
||||
mod format;
|
||||
mod kind;
|
||||
mod postprocessing;
|
||||
|
||||
pub use error::*;
|
||||
pub use format::*;
|
||||
pub use kind::*;
|
||||
|
||||
use postprocessing::*;
|
||||
|
||||
use crate::instance::{data_model, Instance};
|
||||
|
||||
pub type DocumentResult<T> = Result<T, DocumentError>;
|
||||
|
||||
/**
|
||||
A container for [`rbx_dom_weak::WeakDom`] that also takes care of
|
||||
reading and writing different kinds and formats of roblox files.
|
||||
|
||||
---
|
||||
|
||||
### Code Sample #1
|
||||
|
||||
```rust ignore
|
||||
// Reading a document from a file
|
||||
|
||||
let file_path = PathBuf::from("place-file.rbxl");
|
||||
let file_contents = std::fs::read(&file_path)?;
|
||||
|
||||
let document = Document::from_bytes_auto(file_contents)?;
|
||||
|
||||
// Writing a document to a file
|
||||
|
||||
let file_path = PathBuf::from("place-file")
|
||||
.with_extension(document.extension()?);
|
||||
|
||||
std::fs::write(&file_path, document.to_bytes()?)?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Code Sample #2
|
||||
|
||||
```rust ignore
|
||||
// Converting a Document to a DataModel or model child instances
|
||||
let data_model = document.into_data_model_instance()?;
|
||||
|
||||
let model_children = document.into_instance_array()?;
|
||||
|
||||
// Converting a DataModel or model child instances into a Document
|
||||
let place_doc = Document::from_data_model_instance(data_model)?;
|
||||
|
||||
let model_doc = Document::from_instance_array(model_children)?;
|
||||
```
|
||||
*/
|
||||
#[derive(Debug)]
|
||||
pub struct Document {
|
||||
kind: DocumentKind,
|
||||
format: DocumentFormat,
|
||||
dom: WeakDom,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
/**
|
||||
Gets the canonical file extension for a given kind and
|
||||
format of document, which will follow this chart:
|
||||
|
||||
| Kind | Format | Extension |
|
||||
|:------|:-------|:----------|
|
||||
| Place | Binary | `rbxl` |
|
||||
| Place | Xml | `rbxlx` |
|
||||
| Model | Binary | `rbxm` |
|
||||
| Model | Xml | `rbxmx` |
|
||||
*/
|
||||
#[must_use]
|
||||
#[rustfmt::skip]
|
||||
pub fn canonical_extension(kind: DocumentKind, format: DocumentFormat) -> &'static str {
|
||||
match (kind, format) {
|
||||
(DocumentKind::Place, DocumentFormat::Binary) => "rbxl",
|
||||
(DocumentKind::Place, DocumentFormat::Xml) => "rbxlx",
|
||||
(DocumentKind::Model, DocumentFormat::Binary) => "rbxm",
|
||||
(DocumentKind::Model, DocumentFormat::Xml) => "rbxmx",
|
||||
}
|
||||
}
|
||||
|
||||
fn from_bytes_inner(bytes: impl AsRef<[u8]>) -> DocumentResult<(DocumentFormat, WeakDom)> {
|
||||
let bytes = bytes.as_ref();
|
||||
let format = DocumentFormat::from_bytes(bytes).ok_or(DocumentError::UnknownFormat)?;
|
||||
let dom = match format {
|
||||
DocumentFormat::Binary => rbx_binary::from_reader(bytes)
|
||||
.map_err(|err| DocumentError::ReadError(err.to_string())),
|
||||
DocumentFormat::Xml => {
|
||||
let xml_options = XmlDecodeOptions::new()
|
||||
.property_behavior(XmlDecodePropertyBehavior::ReadUnknown);
|
||||
rbx_xml::from_reader(bytes, xml_options)
|
||||
.map_err(|err| DocumentError::ReadError(err.to_string()))
|
||||
}
|
||||
}?;
|
||||
Ok((format, dom))
|
||||
}
|
||||
|
||||
/**
|
||||
Decodes and creates a new document from a byte buffer.
|
||||
|
||||
This will automatically handle and detect if the document should be decoded
|
||||
using a roblox binary or roblox xml format, and if it is a model or place file.
|
||||
|
||||
Note that detection of model vs place file is heavily dependent on the structure
|
||||
of the file, and a model file with services in it will detect as a place file, so
|
||||
if possible using [`Document::from_bytes`] with an explicit kind should be preferred.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the given bytes are not a valid roblox file.
|
||||
*/
|
||||
pub fn from_bytes_auto(bytes: impl AsRef<[u8]>) -> DocumentResult<Self> {
|
||||
let (format, dom) = Self::from_bytes_inner(bytes)?;
|
||||
let kind = DocumentKind::from_weak_dom(&dom).ok_or(DocumentError::UnknownKind)?;
|
||||
Ok(Self { kind, format, dom })
|
||||
}
|
||||
|
||||
/**
|
||||
Decodes and creates a new document from a byte buffer.
|
||||
|
||||
This will automatically handle and detect if the document
|
||||
should be decoded using a roblox binary or roblox xml format.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the given bytes are not a valid roblox file or not of the given kind.
|
||||
*/
|
||||
pub fn from_bytes(bytes: impl AsRef<[u8]>, kind: DocumentKind) -> DocumentResult<Self> {
|
||||
let (format, dom) = Self::from_bytes_inner(bytes)?;
|
||||
Ok(Self { kind, format, dom })
|
||||
}
|
||||
|
||||
/**
|
||||
Encodes the document as a vector of bytes, to
|
||||
be written to a file or sent over the network.
|
||||
|
||||
This will use the same format that the document was created
|
||||
with, meaning if the document is a binary document the output
|
||||
will be binary, and vice versa for xml and other future formats.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the document can not be encoded.
|
||||
*/
|
||||
pub fn to_bytes(&self) -> DocumentResult<Vec<u8>> {
|
||||
self.to_bytes_with_format(self.format)
|
||||
}
|
||||
|
||||
/**
|
||||
Encodes the document as a vector of bytes, to
|
||||
be written to a file or sent over the network.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the document can not be encoded.
|
||||
*/
|
||||
pub fn to_bytes_with_format(&self, format: DocumentFormat) -> DocumentResult<Vec<u8>> {
|
||||
let mut bytes = Vec::new();
|
||||
match format {
|
||||
DocumentFormat::Binary => {
|
||||
rbx_binary::to_writer(&mut bytes, &self.dom, self.dom.root().children())
|
||||
.map_err(|err| DocumentError::WriteError(err.to_string()))
|
||||
}
|
||||
DocumentFormat::Xml => {
|
||||
let xml_options = XmlEncodeOptions::new()
|
||||
.property_behavior(XmlEncodePropertyBehavior::WriteUnknown);
|
||||
rbx_xml::to_writer(
|
||||
&mut bytes,
|
||||
&self.dom,
|
||||
self.dom.root().children(),
|
||||
xml_options,
|
||||
)
|
||||
.map_err(|err| DocumentError::WriteError(err.to_string()))
|
||||
}
|
||||
}?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the kind this document was created with.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn kind(&self) -> DocumentKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the format this document was created with.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn format(&self) -> DocumentFormat {
|
||||
self.format
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the file extension for this document.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn extension(&self) -> &'static str {
|
||||
Self::canonical_extension(self.kind, self.format)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a `DataModel` instance out of this place document.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the document is not a place.
|
||||
*/
|
||||
pub fn into_data_model_instance(mut self) -> DocumentResult<Instance> {
|
||||
if self.kind != DocumentKind::Place {
|
||||
return Err(DocumentError::IntoDataModelInvalidArgs);
|
||||
}
|
||||
|
||||
let dom_root = self.dom.root_ref();
|
||||
|
||||
let data_model_ref = self
|
||||
.dom
|
||||
.insert(dom_root, DomInstanceBuilder::new(data_model::CLASS_NAME));
|
||||
let data_model_child_refs = self.dom.root().children().to_vec();
|
||||
|
||||
for child_ref in data_model_child_refs {
|
||||
if child_ref != data_model_ref {
|
||||
self.dom.transfer_within(child_ref, data_model_ref);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Instance::from_external_dom(&mut self.dom, data_model_ref))
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an array of instances out of this model document.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the document is not a model.
|
||||
*/
|
||||
pub fn into_instance_array(mut self) -> DocumentResult<Vec<Instance>> {
|
||||
if self.kind != DocumentKind::Model {
|
||||
return Err(DocumentError::IntoInstanceArrayInvalidArgs);
|
||||
}
|
||||
|
||||
let dom_child_refs = self.dom.root().children().to_vec();
|
||||
|
||||
let root_child_instances = dom_child_refs
|
||||
.into_iter()
|
||||
.map(|child_ref| Instance::from_external_dom(&mut self.dom, child_ref))
|
||||
.collect();
|
||||
|
||||
Ok(root_child_instances)
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a place document out of a `DataModel` instance.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if the instance is not a `DataModel`.
|
||||
*/
|
||||
pub fn from_data_model_instance(i: Instance) -> DocumentResult<Self> {
|
||||
if i.get_class_name() != data_model::CLASS_NAME {
|
||||
return Err(DocumentError::FromDataModelInvalidArgs);
|
||||
}
|
||||
|
||||
let mut dom = WeakDom::new(DomInstanceBuilder::new("ROOT"));
|
||||
let children: Vec<DomRef> = i
|
||||
.get_children()
|
||||
.iter()
|
||||
.map(|instance| instance.dom_ref)
|
||||
.collect();
|
||||
|
||||
Instance::clone_multiple_into_external_dom(&children, &mut dom);
|
||||
postprocess_dom_for_place(&mut dom);
|
||||
|
||||
Ok(Self {
|
||||
kind: DocumentKind::Place,
|
||||
format: DocumentFormat::default(),
|
||||
dom,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a model document out of an array of instances.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors if any of the instances is a `DataModel`.
|
||||
*/
|
||||
pub fn from_instance_array(v: Vec<Instance>) -> DocumentResult<Self> {
|
||||
for i in &v {
|
||||
if i.get_class_name() == data_model::CLASS_NAME {
|
||||
return Err(DocumentError::FromInstanceArrayInvalidArgs);
|
||||
}
|
||||
}
|
||||
|
||||
let mut dom = WeakDom::new(DomInstanceBuilder::new("ROOT"));
|
||||
let instances: Vec<DomRef> = v.iter().map(|instance| instance.dom_ref).collect();
|
||||
|
||||
Instance::clone_multiple_into_external_dom(&instances, &mut dom);
|
||||
postprocess_dom_for_model(&mut dom);
|
||||
|
||||
Ok(Self {
|
||||
kind: DocumentKind::Model,
|
||||
format: DocumentFormat::default(),
|
||||
dom,
|
||||
})
|
||||
}
|
||||
}
|
48
crates/lune-roblox/src/document/postprocessing.rs
Normal file
|
@ -0,0 +1,48 @@
|
|||
use rbx_dom_weak::{
|
||||
types::{Ref as DomRef, VariantType as DomType},
|
||||
ustr, Instance as DomInstance, WeakDom,
|
||||
};
|
||||
|
||||
use crate::shared::instance::class_is_a;
|
||||
|
||||
pub fn postprocess_dom_for_place(_dom: &mut WeakDom) {
|
||||
// Nothing here yet
|
||||
}
|
||||
|
||||
pub fn postprocess_dom_for_model(dom: &mut WeakDom) {
|
||||
let root_ref = dom.root_ref();
|
||||
recurse_instances(dom, root_ref, &|inst| {
|
||||
// Get rid of some unique ids - roblox does not
|
||||
// save these in model files, and we shouldn't either
|
||||
remove_matching_prop(inst, DomType::UniqueId, "UniqueId");
|
||||
remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
|
||||
// Similar story with ScriptGuid - this is used
|
||||
// in the studio-only cloud script drafts feature
|
||||
if class_is_a(inst.class, "LuaSourceContainer").unwrap_or(false) {
|
||||
inst.properties.remove(&ustr("ScriptGuid"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn recurse_instances<F>(dom: &mut WeakDom, dom_ref: DomRef, f: &F)
|
||||
where
|
||||
F: Fn(&mut DomInstance) + 'static,
|
||||
{
|
||||
let child_refs = match dom.get_by_ref_mut(dom_ref) {
|
||||
Some(inst) => {
|
||||
f(inst);
|
||||
inst.children().to_vec()
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
for child_ref in child_refs {
|
||||
recurse_instances(dom, child_ref, f);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
|
||||
let name = &ustr(name);
|
||||
if inst.properties.get(name).is_some_and(|u| u.ty() == ty) {
|
||||
inst.properties.remove(name);
|
||||
}
|
||||
}
|
68
crates/lune-roblox/src/exports.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
/**
|
||||
Trait for any item that should be exported as part of the `roblox` built-in library.
|
||||
|
||||
This may be an enum or a struct that should export constants and/or constructs.
|
||||
|
||||
### Example usage
|
||||
|
||||
```rs
|
||||
use mlua::prelude::*;
|
||||
|
||||
struct MyType(usize);
|
||||
|
||||
impl MyType {
|
||||
pub fn new(n: usize) -> Self {
|
||||
Self(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for MyType {
|
||||
const EXPORT_NAME: &'static str = "MyType";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let my_type_new = |lua, n: Option<usize>| {
|
||||
Self::new(n.unwrap_or_default())
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", my_type_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for MyType {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
*/
|
||||
pub trait LuaExportsTable<'lua> {
|
||||
const EXPORT_NAME: &'static str;
|
||||
|
||||
fn create_exports_table(lua: &'lua Lua) -> LuaResult<LuaTable<'lua>>;
|
||||
}
|
||||
|
||||
/**
|
||||
Exports a single item that implements the [`LuaExportsTable`] trait.
|
||||
|
||||
Returns the name of the export, as well as the export table.
|
||||
|
||||
### Example usage
|
||||
|
||||
```rs
|
||||
let lua: mlua::Lua::new();
|
||||
|
||||
let (name1, table1) = export::<Type1>(lua)?;
|
||||
let (name2, table2) = export::<Type2>(lua)?;
|
||||
```
|
||||
*/
|
||||
pub fn export<'lua, T>(lua: &'lua Lua) -> LuaResult<(&'static str, LuaValue<'lua>)>
|
||||
where
|
||||
T: LuaExportsTable<'lua>,
|
||||
{
|
||||
Ok((
|
||||
T::EXPORT_NAME,
|
||||
<T as LuaExportsTable>::create_exports_table(lua)?.into_lua(lua)?,
|
||||
))
|
||||
}
|
363
crates/lune-roblox/src/instance/base.rs
Normal file
|
@ -0,0 +1,363 @@
|
|||
#![allow(clippy::items_after_statements)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_dom_weak::{
|
||||
types::{Variant as DomValue, VariantType as DomType},
|
||||
Instance as DomInstance,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
datatypes::{
|
||||
attributes::{ensure_valid_attribute_name, ensure_valid_attribute_value},
|
||||
conversion::{DomValueToLua, LuaToDomValue},
|
||||
types::EnumItem,
|
||||
userdata_impl_eq, userdata_impl_to_string,
|
||||
},
|
||||
shared::instance::{class_is_a, find_property_info},
|
||||
};
|
||||
|
||||
use super::{data_model, registry::InstanceRegistry, Instance};
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
||||
m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| {
|
||||
ensure_not_destroyed(this)?;
|
||||
userdata_impl_to_string(lua, this, ())
|
||||
});
|
||||
m.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
m.add_meta_method(LuaMetaMethod::Index, instance_property_get);
|
||||
m.add_meta_method_mut(LuaMetaMethod::NewIndex, instance_property_set);
|
||||
m.add_method("Clone", |lua, this, ()| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.clone_instance().into_lua(lua)
|
||||
});
|
||||
m.add_method_mut("Destroy", |_, this, ()| {
|
||||
this.destroy();
|
||||
Ok(())
|
||||
});
|
||||
m.add_method_mut("ClearAllChildren", |_, this, ()| {
|
||||
this.clear_all_children();
|
||||
Ok(())
|
||||
});
|
||||
m.add_method("GetChildren", |lua, this, ()| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.get_children().into_lua(lua)
|
||||
});
|
||||
m.add_method("GetDescendants", |lua, this, ()| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.get_descendants().into_lua(lua)
|
||||
});
|
||||
m.add_method("GetFullName", |lua, this, ()| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.get_full_name().into_lua(lua)
|
||||
});
|
||||
m.add_method("GetDebugId", |lua, this, ()| {
|
||||
this.dom_ref.to_string().into_lua(lua)
|
||||
});
|
||||
m.add_method("FindFirstAncestor", |lua, this, name: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.find_ancestor(|child| child.name == name).into_lua(lua)
|
||||
});
|
||||
m.add_method(
|
||||
"FindFirstAncestorOfClass",
|
||||
|lua, this, class_name: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.find_ancestor(|child| child.class == class_name)
|
||||
.into_lua(lua)
|
||||
},
|
||||
);
|
||||
m.add_method(
|
||||
"FindFirstAncestorWhichIsA",
|
||||
|lua, this, class_name: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.find_ancestor(|child| class_is_a(child.class, &class_name).unwrap_or(false))
|
||||
.into_lua(lua)
|
||||
},
|
||||
);
|
||||
m.add_method(
|
||||
"FindFirstChild",
|
||||
|lua, this, (name, recursive): (String, Option<bool>)| {
|
||||
ensure_not_destroyed(this)?;
|
||||
let predicate = |child: &DomInstance| child.name == name;
|
||||
if matches!(recursive, Some(true)) {
|
||||
this.find_descendant(predicate).into_lua(lua)
|
||||
} else {
|
||||
this.find_child(predicate).into_lua(lua)
|
||||
}
|
||||
},
|
||||
);
|
||||
m.add_method(
|
||||
"FindFirstChildOfClass",
|
||||
|lua, this, (class_name, recursive): (String, Option<bool>)| {
|
||||
ensure_not_destroyed(this)?;
|
||||
let predicate = |child: &DomInstance| child.class == class_name;
|
||||
if matches!(recursive, Some(true)) {
|
||||
this.find_descendant(predicate).into_lua(lua)
|
||||
} else {
|
||||
this.find_child(predicate).into_lua(lua)
|
||||
}
|
||||
},
|
||||
);
|
||||
m.add_method(
|
||||
"FindFirstChildWhichIsA",
|
||||
|lua, this, (class_name, recursive): (String, Option<bool>)| {
|
||||
ensure_not_destroyed(this)?;
|
||||
let predicate =
|
||||
|child: &DomInstance| class_is_a(child.class, &class_name).unwrap_or(false);
|
||||
if matches!(recursive, Some(true)) {
|
||||
this.find_descendant(predicate).into_lua(lua)
|
||||
} else {
|
||||
this.find_child(predicate).into_lua(lua)
|
||||
}
|
||||
},
|
||||
);
|
||||
m.add_method("IsA", |_, this, class_name: String| {
|
||||
Ok(class_is_a(this.class_name, class_name).unwrap_or(false))
|
||||
});
|
||||
m.add_method(
|
||||
"IsAncestorOf",
|
||||
|_, this, instance: LuaUserDataRef<Instance>| {
|
||||
ensure_not_destroyed(this)?;
|
||||
Ok(instance
|
||||
.find_ancestor(|ancestor| ancestor.referent() == this.dom_ref)
|
||||
.is_some())
|
||||
},
|
||||
);
|
||||
m.add_method(
|
||||
"IsDescendantOf",
|
||||
|_, this, instance: LuaUserDataRef<Instance>| {
|
||||
ensure_not_destroyed(this)?;
|
||||
Ok(this
|
||||
.find_ancestor(|ancestor| ancestor.referent() == instance.dom_ref)
|
||||
.is_some())
|
||||
},
|
||||
);
|
||||
m.add_method("GetAttribute", |lua, this, name: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
match this.get_attribute(name) {
|
||||
Some(attribute) => Ok(LuaValue::dom_value_to_lua(lua, &attribute)?),
|
||||
None => Ok(LuaValue::Nil),
|
||||
}
|
||||
});
|
||||
m.add_method("GetAttributes", |lua, this, ()| {
|
||||
ensure_not_destroyed(this)?;
|
||||
let attributes = this.get_attributes();
|
||||
let tab = lua.create_table_with_capacity(0, attributes.len())?;
|
||||
for (key, value) in attributes {
|
||||
tab.set(key, LuaValue::dom_value_to_lua(lua, &value)?)?;
|
||||
}
|
||||
Ok(tab)
|
||||
});
|
||||
m.add_method(
|
||||
"SetAttribute",
|
||||
|lua, this, (attribute_name, lua_value): (String, LuaValue)| {
|
||||
ensure_not_destroyed(this)?;
|
||||
ensure_valid_attribute_name(&attribute_name)?;
|
||||
if lua_value.is_nil() || lua_value.is_null() {
|
||||
this.remove_attribute(attribute_name);
|
||||
Ok(())
|
||||
} else {
|
||||
match lua_value.lua_to_dom_value(lua, None) {
|
||||
Ok(dom_value) => {
|
||||
ensure_valid_attribute_value(&dom_value)?;
|
||||
this.set_attribute(attribute_name, dom_value);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
m.add_method("GetTags", |_, this, ()| {
|
||||
ensure_not_destroyed(this)?;
|
||||
Ok(this.get_tags())
|
||||
});
|
||||
m.add_method("HasTag", |_, this, tag: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
Ok(this.has_tag(tag))
|
||||
});
|
||||
m.add_method("AddTag", |_, this, tag: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.add_tag(tag);
|
||||
Ok(())
|
||||
});
|
||||
m.add_method("RemoveTag", |_, this, tag: String| {
|
||||
ensure_not_destroyed(this)?;
|
||||
this.remove_tag(tag);
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
fn ensure_not_destroyed(inst: &Instance) -> LuaResult<()> {
|
||||
if inst.is_destroyed() {
|
||||
Err(LuaError::RuntimeError(
|
||||
"Instance has been destroyed".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Gets a property value for an instance.
|
||||
|
||||
Getting a value does the following:
|
||||
|
||||
1. Check if it is a special property like "ClassName", "Name" or "Parent"
|
||||
2. Check if a property exists for the wanted name
|
||||
2a. Get an existing instance property OR
|
||||
2b. Get a property from a known default value
|
||||
3. Get a current child of the instance
|
||||
4. No valid property or instance found, throw error
|
||||
*/
|
||||
fn instance_property_get<'lua>(
|
||||
lua: &'lua Lua,
|
||||
this: &Instance,
|
||||
prop_name: String,
|
||||
) -> LuaResult<LuaValue<'lua>> {
|
||||
match prop_name.as_str() {
|
||||
"ClassName" => return this.get_class_name().into_lua(lua),
|
||||
"Parent" => {
|
||||
return this.get_parent().into_lua(lua);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
ensure_not_destroyed(this)?;
|
||||
|
||||
if prop_name.as_str() == "Name" {
|
||||
return this.get_name().into_lua(lua);
|
||||
}
|
||||
|
||||
if let Some(info) = find_property_info(this.class_name, &prop_name) {
|
||||
if let Some(prop) = this.get_property(&prop_name) {
|
||||
if let DomValue::Enum(enum_value) = prop {
|
||||
let enum_name = info.enum_name.ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get property '{prop_name}' - encountered unknown enum",
|
||||
))
|
||||
})?;
|
||||
EnumItem::from_enum_name_and_value(&enum_name, enum_value.to_u32())
|
||||
.ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get property '{}' - Enum.{} does not contain numeric value {}",
|
||||
prop_name, enum_name, enum_value.to_u32()
|
||||
))
|
||||
})?
|
||||
.into_lua(lua)
|
||||
} else {
|
||||
Ok(LuaValue::dom_value_to_lua(lua, &prop)?)
|
||||
}
|
||||
} else if let (Some(enum_name), Some(enum_value)) = (info.enum_name, info.enum_default) {
|
||||
EnumItem::from_enum_name_and_value(&enum_name, enum_value)
|
||||
.ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get property '{prop_name}' - Enum.{enum_name} does not contain numeric value {enum_value}",
|
||||
))
|
||||
})?
|
||||
.into_lua(lua)
|
||||
} else if let Some(prop_default) = info.value_default {
|
||||
Ok(LuaValue::dom_value_to_lua(lua, prop_default)?)
|
||||
} else if info.value_type.is_some() {
|
||||
if info.value_type == Some(DomType::Ref) {
|
||||
Ok(LuaValue::Nil)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Failed to get property '{prop_name}' - missing default value",
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Failed to get property '{prop_name}' - malformed property info",
|
||||
)))
|
||||
}
|
||||
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
|
||||
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
|
||||
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
|
||||
getter.call(*this)
|
||||
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
|
||||
Ok(LuaValue::Function(method))
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{prop_name} is not a valid member of {this}",
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Sets a property value for an instance.
|
||||
|
||||
Setting a value does the following:
|
||||
|
||||
1. Check if it is a special property like "ClassName", "Name" or "Parent"
|
||||
2. Check if a property exists for the wanted name
|
||||
2a. Set a strict enum from a given EnumItem OR
|
||||
2b. Set a normal property from a given value
|
||||
*/
|
||||
fn instance_property_set<'lua>(
|
||||
lua: &'lua Lua,
|
||||
this: &mut Instance,
|
||||
(prop_name, prop_value): (String, LuaValue<'lua>),
|
||||
) -> LuaResult<()> {
|
||||
ensure_not_destroyed(this)?;
|
||||
|
||||
match prop_name.as_str() {
|
||||
"ClassName" => {
|
||||
return Err(LuaError::RuntimeError(
|
||||
"Failed to set ClassName - property is read-only".to_string(),
|
||||
));
|
||||
}
|
||||
"Name" => {
|
||||
let name = String::from_lua(prop_value, lua)?;
|
||||
this.set_name(name);
|
||||
return Ok(());
|
||||
}
|
||||
"Parent" => {
|
||||
if this.get_class_name() == data_model::CLASS_NAME {
|
||||
return Err(LuaError::RuntimeError(
|
||||
"Failed to set Parent - DataModel can not be reparented".to_string(),
|
||||
));
|
||||
}
|
||||
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
|
||||
let parent = Parent::from_lua(prop_value, lua)?;
|
||||
this.set_parent(parent.map(|p| *p));
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(info) = find_property_info(this.class_name, &prop_name) {
|
||||
if let Some(enum_name) = info.enum_name {
|
||||
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
|
||||
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
|
||||
this.set_property(prop_name, DomValue::EnumItem((*given_enum).clone().into()));
|
||||
Ok(())
|
||||
}
|
||||
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
|
||||
"Failed to set property '{}' - expected Enum.{}, got Enum.{}",
|
||||
prop_name, enum_name, given_enum.parent.desc.name
|
||||
))),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
} else if let Some(dom_type) = info.value_type {
|
||||
match prop_value.lua_to_dom_value(lua, Some(dom_type)) {
|
||||
Ok(dom_value) => {
|
||||
this.set_property(prop_name, dom_value);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Failed to set property '{prop_name}' - malformed property info",
|
||||
)))
|
||||
}
|
||||
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
|
||||
setter.call((*this, prop_value))
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{prop_name} is not a valid member of {this}",
|
||||
)))
|
||||
}
|
||||
}
|
77
crates/lune-roblox/src/instance/data_model.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use crate::shared::{
|
||||
classes::{
|
||||
add_class_restricted_getter, add_class_restricted_method,
|
||||
get_or_create_property_ref_instance,
|
||||
},
|
||||
instance::class_is_a_service,
|
||||
};
|
||||
|
||||
use super::Instance;
|
||||
|
||||
pub const CLASS_NAME: &str = "DataModel";
|
||||
|
||||
pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
|
||||
add_class_restricted_getter(f, CLASS_NAME, "Workspace", data_model_get_workspace);
|
||||
}
|
||||
|
||||
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|
||||
add_class_restricted_method(m, CLASS_NAME, "GetService", data_model_get_service);
|
||||
add_class_restricted_method(m, CLASS_NAME, "FindService", data_model_find_service);
|
||||
}
|
||||
|
||||
/**
|
||||
Get the workspace parented under this datamodel, or create it if it doesn't exist.
|
||||
|
||||
### See Also
|
||||
* [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult<Instance> {
|
||||
get_or_create_property_ref_instance(this, "Workspace", "Workspace")
|
||||
}
|
||||
|
||||
/**
|
||||
Gets or creates a service for this `DataModel`.
|
||||
|
||||
### See Also
|
||||
* [`GetService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#GetService)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> LuaResult<Instance> {
|
||||
if matches!(class_is_a_service(&service_name), None | Some(false)) {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"'{service_name}' is not a valid service name",
|
||||
)))
|
||||
} else if let Some(service) = this.find_child(|child| child.class == service_name) {
|
||||
Ok(service)
|
||||
} else {
|
||||
let service = Instance::new_orphaned(service_name);
|
||||
service.set_parent(Some(*this));
|
||||
Ok(service)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Gets a service for this `DataModel`, if it exists.
|
||||
|
||||
### See Also
|
||||
* [`FindService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#FindService)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn data_model_find_service(
|
||||
_: &Lua,
|
||||
this: &Instance,
|
||||
service_name: String,
|
||||
) -> LuaResult<Option<Instance>> {
|
||||
if matches!(class_is_a_service(&service_name), None | Some(false)) {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"'{service_name}' is not a valid service name",
|
||||
)))
|
||||
} else if let Some(service) = this.find_child(|child| child.class == service_name) {
|
||||
Ok(Some(service))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
803
crates/lune-roblox/src/instance/mod.rs
Normal file
|
@ -0,0 +1,803 @@
|
|||
#![allow(clippy::missing_panics_doc)]
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
fmt,
|
||||
hash::{Hash, Hasher},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use rbx_dom_weak::{
|
||||
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
|
||||
ustr, Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, Ustr, WeakDom,
|
||||
};
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use crate::{
|
||||
exports::LuaExportsTable,
|
||||
shared::instance::{class_exists, class_is_a},
|
||||
};
|
||||
|
||||
pub(crate) mod base;
|
||||
pub(crate) mod data_model;
|
||||
pub(crate) mod terrain;
|
||||
pub(crate) mod workspace;
|
||||
|
||||
pub mod registry;
|
||||
|
||||
const PROPERTY_NAME_ATTRIBUTES: &str = "Attributes";
|
||||
const PROPERTY_NAME_TAGS: &str = "Tags";
|
||||
|
||||
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
|
||||
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Instance {
|
||||
pub(crate) dom_ref: DomRef,
|
||||
pub(crate) class_name: Ustr,
|
||||
}
|
||||
|
||||
impl Instance {
|
||||
/**
|
||||
Creates a new `Instance` from an existing dom object ref.
|
||||
|
||||
Panics if the instance does not exist in the internal dom,
|
||||
or if the given dom object ref points to the internal dom root.
|
||||
|
||||
**WARNING:** Creating a new instance requires locking the internal dom,
|
||||
any existing lock must first be released to prevent any deadlocking.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn new(dom_ref: DomRef) -> Self {
|
||||
Self::new_opt(dom_ref).expect("Failed to find instance in document")
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new `Instance` from a dom object ref, if the instance exists.
|
||||
|
||||
Panics if the given dom object ref points to the internal dom root.
|
||||
|
||||
**WARNING:** Creating a new instance requires locking the internal dom,
|
||||
any existing lock must first be released to prevent any deadlocking.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn new_opt(dom_ref: DomRef) -> Option<Self> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
if let Some(instance) = dom.get_by_ref(dom_ref) {
|
||||
assert!(
|
||||
!(instance.referent() == dom.root_ref()),
|
||||
"Instances can not be created from dom roots"
|
||||
);
|
||||
|
||||
Some(Self {
|
||||
dom_ref,
|
||||
class_name: instance.class,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new orphaned `Instance` with a given class name.
|
||||
|
||||
An orphaned instance is an instance at the root of Lune's internal weak dom.
|
||||
|
||||
**WARNING:** Creating a new instance requires locking the internal dom,
|
||||
any existing lock must first be released to prevent any deadlocking.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn new_orphaned(class_name: impl AsRef<str>) -> Self {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let class_name = class_name.as_ref();
|
||||
|
||||
let instance = DomInstanceBuilder::new(class_name);
|
||||
|
||||
let dom_root = dom.root_ref();
|
||||
let dom_ref = dom.insert(dom_root, instance);
|
||||
|
||||
Self {
|
||||
dom_ref,
|
||||
class_name: ustr(class_name),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new orphaned `Instance` by transferring
|
||||
it from an external weak dom to the internal one.
|
||||
|
||||
An orphaned instance is an instance at the root of Lune's internal weak dom.
|
||||
|
||||
Panics if the given dom ref is the root dom ref of the external weak dom.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn from_external_dom(external_dom: &mut WeakDom, external_dom_ref: DomRef) -> Self {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let dom_root = dom.root_ref();
|
||||
|
||||
external_dom.transfer(external_dom_ref, &mut dom, dom_root);
|
||||
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
Self::new(external_dom_ref)
|
||||
}
|
||||
|
||||
/**
|
||||
Clones an instance to an external weak dom.
|
||||
|
||||
This will place the instance as a child of the
|
||||
root of the weak dom, and return its referent.
|
||||
*/
|
||||
pub fn clone_into_external_dom(self, external_dom: &mut WeakDom) -> DomRef {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let cloned = dom.clone_into_external(self.dom_ref, external_dom);
|
||||
external_dom.transfer_within(cloned, external_dom.root_ref());
|
||||
|
||||
cloned
|
||||
}
|
||||
|
||||
/**
|
||||
Clones multiple instances to an external weak dom.
|
||||
|
||||
This will place the instances as children of the
|
||||
root of the weak dom, and return their referents.
|
||||
*/
|
||||
pub fn clone_multiple_into_external_dom(
|
||||
referents: &[DomRef],
|
||||
external_dom: &mut WeakDom,
|
||||
) -> Vec<DomRef> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let cloned = dom.clone_multiple_into_external(referents, external_dom);
|
||||
|
||||
for referent in &cloned {
|
||||
external_dom.transfer_within(*referent, external_dom.root_ref());
|
||||
}
|
||||
|
||||
cloned
|
||||
}
|
||||
|
||||
/**
|
||||
Clones the instance and all of its descendants, and orphans it.
|
||||
|
||||
To then save the new instance it must be re-parented,
|
||||
which matches the exact behavior of Roblox's instances.
|
||||
|
||||
### See Also
|
||||
* [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn clone_instance(&self) -> Self {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let new_ref = dom.clone_within(self.dom_ref);
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
|
||||
let new_inst = Self::new(new_ref);
|
||||
new_inst.set_parent(None);
|
||||
new_inst
|
||||
}
|
||||
|
||||
/**
|
||||
Destroys the instance, removing it completely
|
||||
from the weak dom with no way of recovering it.
|
||||
|
||||
All member methods will throw errors when called from lua and panic
|
||||
when called from rust after the instance has been destroyed.
|
||||
|
||||
Returns `true` if destroyed successfully, `false` if already destroyed.
|
||||
|
||||
### See Also
|
||||
* [`Destroy`](https://create.roblox.com/docs/reference/engine/classes/Instance#Destroy)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn destroy(&mut self) -> bool {
|
||||
if self.is_destroyed() {
|
||||
false
|
||||
} else {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
dom.destroy(self.dom_ref);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn is_destroyed(&self) -> bool {
|
||||
// NOTE: This property can not be cached since instance references
|
||||
// other than this one may have destroyed this one, and we don't
|
||||
// keep track of all current instance reference structs
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
dom.get_by_ref(self.dom_ref).is_none()
|
||||
}
|
||||
|
||||
/**
|
||||
Destroys all child instances.
|
||||
|
||||
### See Also
|
||||
* [`Instance::Destroy`] for more info about what happens when an instance gets destroyed
|
||||
* [`ClearAllChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClearAllChildren)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn clear_all_children(&mut self) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let instance = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
|
||||
let child_refs = instance.children().to_vec();
|
||||
for child_ref in child_refs {
|
||||
dom.destroy(child_ref);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if the instance matches or inherits a given class name.
|
||||
|
||||
### See Also
|
||||
* [`IsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
|
||||
class_is_a(self.class_name, class_name).unwrap_or(false)
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the class name of the instance.
|
||||
|
||||
This will return the correct class name even if the instance has been destroyed.
|
||||
|
||||
### See Also
|
||||
* [`ClassName`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClassName)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_class_name(&self) -> &str {
|
||||
self.class_name.as_str()
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the name of the instance, if it exists.
|
||||
|
||||
### See Also
|
||||
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_name(&self) -> String {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
dom.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.name
|
||||
.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the name of the instance, if it exists.
|
||||
|
||||
### See Also
|
||||
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn set_name(&self, name: impl Into<String>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
dom.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.name = name.into();
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the parent of the instance, if it exists.
|
||||
|
||||
### See Also
|
||||
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_parent(&self) -> Option<Instance> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let parent_ref = dom.get_by_ref(self.dom_ref)?.parent();
|
||||
|
||||
if parent_ref == dom.root_ref() {
|
||||
None
|
||||
} else {
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
Some(Self::new(parent_ref))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the parent of the instance, if it exists.
|
||||
|
||||
If the provided parent is [`None`] the instance will become orphaned.
|
||||
|
||||
An orphaned instance is an instance at the root of Lune's internal weak dom.
|
||||
|
||||
### See Also
|
||||
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn set_parent(&self, parent: Option<Instance>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let parent_ref = parent.map_or_else(|| dom.root_ref(), |parent| parent.dom_ref);
|
||||
|
||||
dom.transfer_within(self.dom_ref, parent_ref);
|
||||
}
|
||||
|
||||
/**
|
||||
Gets a property for the instance, if it exists.
|
||||
*/
|
||||
pub fn get_property(&self, name: impl AsRef<str>) -> Option<DomValue> {
|
||||
INTERNAL_DOM
|
||||
.lock()
|
||||
.expect("Failed to lock document")
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.properties
|
||||
.get(&ustr(name.as_ref()))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/**
|
||||
Sets a property for the instance.
|
||||
|
||||
Note that setting a property here will not fail even if the
|
||||
property does not actually exist for the instance class.
|
||||
*/
|
||||
pub fn set_property(&self, name: impl AsRef<str>, value: DomValue) {
|
||||
INTERNAL_DOM
|
||||
.lock()
|
||||
.expect("Failed to lock document")
|
||||
.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.properties
|
||||
.insert(ustr(name.as_ref()), value);
|
||||
}
|
||||
|
||||
/**
|
||||
Gets an attribute for the instance, if it exists.
|
||||
|
||||
### See Also
|
||||
* [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_attribute(&self, name: impl AsRef<str>) -> Option<DomValue> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Attributes(attributes)) =
|
||||
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||
{
|
||||
attributes.get(name.as_ref()).cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Gets all known attributes for the instance.
|
||||
|
||||
### See Also
|
||||
* [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_attributes(&self) -> BTreeMap<String, DomValue> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Attributes(attributes)) =
|
||||
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||
{
|
||||
attributes.clone().into_iter().collect()
|
||||
} else {
|
||||
BTreeMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Sets an attribute for the instance.
|
||||
|
||||
### See Also
|
||||
* [`SetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn set_attribute(&self, name: impl AsRef<str>, value: DomValue) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
// NOTE: Attributes do not support integers, only floats
|
||||
let value = match value {
|
||||
DomValue::Int32(i) => DomValue::Float32(i as f32),
|
||||
DomValue::Int64(i) => DomValue::Float64(i as f64),
|
||||
value => value,
|
||||
};
|
||||
if let Some(DomValue::Attributes(attributes)) =
|
||||
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||
{
|
||||
attributes.insert(name.as_ref().to_string(), value);
|
||||
} else {
|
||||
let mut attributes = DomAttributes::new();
|
||||
attributes.insert(name.as_ref().to_string(), value);
|
||||
inst.properties.insert(
|
||||
ustr(PROPERTY_NAME_ATTRIBUTES),
|
||||
DomValue::Attributes(attributes),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes an attribute from the instance.
|
||||
|
||||
Note that this does not have an equivalent in the Roblox engine API,
|
||||
but separating this from `set_attribute` lets `set_attribute` be more
|
||||
ergonomic and not require an `Option<DomValue>` for the value argument.
|
||||
The equivalent in the Roblox engine API would be `instance:SetAttribute(name, nil)`.
|
||||
*/
|
||||
pub fn remove_attribute(&self, name: impl AsRef<str>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Attributes(attributes)) =
|
||||
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
|
||||
{
|
||||
attributes.remove(name.as_ref());
|
||||
if attributes.is_empty() {
|
||||
inst.properties.remove(&ustr(PROPERTY_NAME_ATTRIBUTES));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Adds a tag to the instance.
|
||||
|
||||
### See Also
|
||||
* [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn add_tag(&self, name: impl AsRef<str>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
|
||||
tags.push(name.as_ref());
|
||||
} else {
|
||||
inst.properties.insert(
|
||||
ustr(PROPERTY_NAME_TAGS),
|
||||
DomValue::Tags(vec![name.as_ref().to_string()].into()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Gets all current tags for the instance.
|
||||
|
||||
### See Also
|
||||
* [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_tags(&self) -> Vec<String> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
|
||||
tags.iter().map(ToString::to_string).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if the instance has a specific tag.
|
||||
|
||||
### See Also
|
||||
* [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn has_tag(&self, name: impl AsRef<str>) -> bool {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
|
||||
let name = name.as_ref();
|
||||
tags.iter().any(|tag| tag == name)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Removes a tag from the instance.
|
||||
|
||||
### See Also
|
||||
* [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn remove_tag(&self, name: impl AsRef<str>) {
|
||||
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let inst = dom
|
||||
.get_by_ref_mut(self.dom_ref)
|
||||
.expect("Failed to find instance in document");
|
||||
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
|
||||
let name = name.as_ref();
|
||||
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
|
||||
new_tags.retain(|tag| tag != name);
|
||||
inst.properties
|
||||
.insert(ustr(PROPERTY_NAME_TAGS), DomValue::Tags(new_tags.into()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Gets all of the current children of this `Instance`.
|
||||
|
||||
Note that this is a somewhat expensive operation and that other
|
||||
operations using weak dom referents should be preferred if possible.
|
||||
|
||||
### See Also
|
||||
* [`GetChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetChildren)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_children(&self) -> Vec<Instance> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let children = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.children()
|
||||
.to_vec();
|
||||
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
children.into_iter().map(Self::new).collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Gets all of the current descendants of this `Instance` using a breadth-first search.
|
||||
|
||||
Note that this is a somewhat expensive operation and that other
|
||||
operations using weak dom referents should be preferred if possible.
|
||||
|
||||
### See Also
|
||||
* [`GetDescendants`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetDescendants)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_descendants(&self) -> Vec<Instance> {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let mut descendants = Vec::new();
|
||||
let mut queue = VecDeque::from_iter(
|
||||
dom.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.children(),
|
||||
);
|
||||
|
||||
while let Some(queue_ref) = queue.pop_front() {
|
||||
descendants.push(*queue_ref);
|
||||
let queue_inst = dom.get_by_ref(*queue_ref).unwrap();
|
||||
for queue_ref_inner in queue_inst.children().iter().rev() {
|
||||
queue.push_back(queue_ref_inner);
|
||||
}
|
||||
}
|
||||
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
descendants.into_iter().map(Self::new).collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the "full name" of this instance.
|
||||
|
||||
This will be a path composed of instance names from the top-level
|
||||
ancestor of this instance down to itself, in the following format:
|
||||
|
||||
`Ancestor.Child.Descendant.Instance`
|
||||
|
||||
### See Also
|
||||
* [`GetFullName`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetFullName)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn get_full_name(&self) -> String {
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
let dom_root = dom.root_ref();
|
||||
|
||||
let mut parts = Vec::new();
|
||||
let mut instance_ref = self.dom_ref;
|
||||
|
||||
while let Some(instance) = dom.get_by_ref(instance_ref) {
|
||||
if instance_ref != dom_root && instance.class != data_model::CLASS_NAME {
|
||||
instance_ref = instance.parent();
|
||||
parts.push(instance.name.clone());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
parts.reverse();
|
||||
parts.join(".")
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a child of the instance using the given predicate callback.
|
||||
|
||||
### See Also
|
||||
* [`FindFirstChild`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChild) on the Roblox Developer Hub
|
||||
* [`FindFirstChildOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildOfClass) on the Roblox Developer Hub
|
||||
* [`FindFirstChildWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstChildWhichIsA) on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn find_child<F>(&self, predicate: F) -> Option<Instance>
|
||||
where
|
||||
F: Fn(&DomInstance) -> bool,
|
||||
{
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let children = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.children()
|
||||
.to_vec();
|
||||
|
||||
let found_ref = children.into_iter().find(|child_ref| {
|
||||
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
|
||||
predicate(child_inst)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
found_ref.map(Self::new)
|
||||
}
|
||||
|
||||
/**
|
||||
Finds an ancestor of the instance using the given predicate callback.
|
||||
|
||||
### See Also
|
||||
* [`FindFirstAncestor`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestor) on the Roblox Developer Hub
|
||||
* [`FindFirstAncestorOfClass`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorOfClass) on the Roblox Developer Hub
|
||||
* [`FindFirstAncestorWhichIsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstAncestorWhichIsA) on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn find_ancestor<F>(&self, predicate: F) -> Option<Instance>
|
||||
where
|
||||
F: Fn(&DomInstance) -> bool,
|
||||
{
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let mut ancestor_ref = dom
|
||||
.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.parent();
|
||||
|
||||
while let Some(ancestor) = dom.get_by_ref(ancestor_ref) {
|
||||
if predicate(ancestor) {
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
return Some(Self::new(ancestor_ref));
|
||||
}
|
||||
ancestor_ref = ancestor.parent();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a descendant of the instance using the given
|
||||
predicate callback and a breadth-first search.
|
||||
|
||||
### See Also
|
||||
* [`FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
pub fn find_descendant<F>(&self, predicate: F) -> Option<Instance>
|
||||
where
|
||||
F: Fn(&DomInstance) -> bool,
|
||||
{
|
||||
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
|
||||
|
||||
let mut queue = VecDeque::from_iter(
|
||||
dom.get_by_ref(self.dom_ref)
|
||||
.expect("Failed to find instance in document")
|
||||
.children(),
|
||||
);
|
||||
|
||||
while let Some(queue_item) = queue
|
||||
.pop_front()
|
||||
.and_then(|queue_ref| dom.get_by_ref(*queue_ref))
|
||||
{
|
||||
if predicate(queue_item) {
|
||||
let queue_ref = queue_item.referent();
|
||||
drop(dom); // Self::new needs mutex handle, drop it first
|
||||
return Some(Self::new(queue_ref));
|
||||
}
|
||||
queue.extend(queue_item.children());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaExportsTable<'_> for Instance {
|
||||
const EXPORT_NAME: &'static str = "Instance";
|
||||
|
||||
fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
let instance_new = |lua, class_name: String| {
|
||||
if class_exists(&class_name) {
|
||||
Instance::new_orphaned(class_name).into_lua(lua)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"Failed to create Instance - '{class_name}' is not a valid class name",
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("new", instance_new)?
|
||||
.build_readonly()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Here we add inheritance-like behavior for instances by creating
|
||||
fields that are restricted to specific classnames / base classes
|
||||
|
||||
Note that we should try to be conservative with how many classes
|
||||
and methods we support here - we should only implement methods that
|
||||
are necessary for modifying the dom and / or having ergonomic access
|
||||
to the dom, not try to replicate Roblox engine behavior of instances
|
||||
|
||||
If a user wants to replicate Roblox engine behavior, they can use the
|
||||
instance registry, and register properties + methods from the lua side
|
||||
*/
|
||||
impl LuaUserData for Instance {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
data_model::add_fields(fields);
|
||||
workspace::add_fields(fields);
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
base::add_methods(methods);
|
||||
data_model::add_methods(methods);
|
||||
terrain::add_methods(methods);
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Instance {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.dom_ref.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Instance {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
if self.is_destroyed() {
|
||||
"<<DESTROYED>>".to_string()
|
||||
} else {
|
||||
self.get_name()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Instance {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.dom_ref == other.dom_ref
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Instance> for DomRef {
|
||||
fn from(value: Instance) -> Self {
|
||||
value.dom_ref
|
||||
}
|
||||
}
|
274
crates/lune-roblox/src/instance/registry.rs
Normal file
|
@ -0,0 +1,274 @@
|
|||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use mlua::{prelude::*, AppDataRef};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::Instance;
|
||||
|
||||
type InstanceRegistryMap = HashMap<String, HashMap<String, LuaRegistryKey>>;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum InstanceRegistryError {
|
||||
#[error("class name '{0}' is not valid")]
|
||||
InvalidClassName(String),
|
||||
#[error("class '{class_name}' already registered method '{method_name}'")]
|
||||
MethodAlreadyExists {
|
||||
class_name: String,
|
||||
method_name: String,
|
||||
},
|
||||
#[error("class '{class_name}' already registered property '{property_name}'")]
|
||||
PropertyAlreadyExists {
|
||||
class_name: String,
|
||||
property_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InstanceRegistry {
|
||||
getters: Arc<Mutex<InstanceRegistryMap>>,
|
||||
setters: Arc<Mutex<InstanceRegistryMap>>,
|
||||
methods: Arc<Mutex<InstanceRegistryMap>>,
|
||||
}
|
||||
|
||||
impl InstanceRegistry {
|
||||
// NOTE: We lazily create the instance registry instead
|
||||
// of always creating it together with the roblox builtin
|
||||
// since it is less commonly used and it simplifies some app
|
||||
// data borrowing relationship problems we'd otherwise have
|
||||
fn get_or_create(lua: &Lua) -> AppDataRef<'_, Self> {
|
||||
if lua.app_data_ref::<Self>().is_none() {
|
||||
lua.set_app_data(Self {
|
||||
getters: Arc::new(Mutex::new(HashMap::new())),
|
||||
setters: Arc::new(Mutex::new(HashMap::new())),
|
||||
methods: Arc::new(Mutex::new(HashMap::new())),
|
||||
});
|
||||
}
|
||||
lua.app_data_ref::<Self>()
|
||||
.expect("Missing InstanceRegistry in app data")
|
||||
}
|
||||
|
||||
/**
|
||||
Inserts a method into the instance registry.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the method already exists in the registry.
|
||||
*/
|
||||
pub fn insert_method<'lua>(
|
||||
lua: &'lua Lua,
|
||||
class_name: &str,
|
||||
method_name: &str,
|
||||
method: LuaFunction<'lua>,
|
||||
) -> Result<(), InstanceRegistryError> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
|
||||
let mut methods = registry
|
||||
.methods
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry methods");
|
||||
|
||||
let class_methods = methods.entry(class_name.to_string()).or_default();
|
||||
if class_methods.contains_key(method_name) {
|
||||
return Err(InstanceRegistryError::MethodAlreadyExists {
|
||||
class_name: class_name.to_string(),
|
||||
method_name: method_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let key = lua
|
||||
.create_registry_value(method)
|
||||
.expect("Failed to store method in lua registry");
|
||||
class_methods.insert(method_name.to_string(), key);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/**
|
||||
Inserts a property getter into the instance registry.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the property already exists in the registry.
|
||||
*/
|
||||
pub fn insert_property_getter<'lua>(
|
||||
lua: &'lua Lua,
|
||||
class_name: &str,
|
||||
property_name: &str,
|
||||
property_getter: LuaFunction<'lua>,
|
||||
) -> Result<(), InstanceRegistryError> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
|
||||
let mut getters = registry
|
||||
.getters
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry getters");
|
||||
|
||||
let class_getters = getters.entry(class_name.to_string()).or_default();
|
||||
if class_getters.contains_key(property_name) {
|
||||
return Err(InstanceRegistryError::PropertyAlreadyExists {
|
||||
class_name: class_name.to_string(),
|
||||
property_name: property_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let key = lua
|
||||
.create_registry_value(property_getter)
|
||||
.expect("Failed to store getter in lua registry");
|
||||
class_getters.insert(property_name.to_string(), key);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/**
|
||||
Inserts a property setter into the instance registry.
|
||||
|
||||
# Errors
|
||||
|
||||
- If the property already exists in the registry.
|
||||
*/
|
||||
pub fn insert_property_setter<'lua>(
|
||||
lua: &'lua Lua,
|
||||
class_name: &str,
|
||||
property_name: &str,
|
||||
property_setter: LuaFunction<'lua>,
|
||||
) -> Result<(), InstanceRegistryError> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
|
||||
let mut setters = registry
|
||||
.setters
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry getters");
|
||||
|
||||
let class_setters = setters.entry(class_name.to_string()).or_default();
|
||||
if class_setters.contains_key(property_name) {
|
||||
return Err(InstanceRegistryError::PropertyAlreadyExists {
|
||||
class_name: class_name.to_string(),
|
||||
property_name: property_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let key = lua
|
||||
.create_registry_value(property_setter)
|
||||
.expect("Failed to store getter in lua registry");
|
||||
class_setters.insert(property_name.to_string(), key);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a method in the instance registry.
|
||||
|
||||
Returns `None` if the method is not found.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn find_method<'lua>(
|
||||
lua: &'lua Lua,
|
||||
instance: &Instance,
|
||||
method_name: &str,
|
||||
) -> Option<LuaFunction<'lua>> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
let methods = registry
|
||||
.methods
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry methods");
|
||||
|
||||
class_name_chain(&instance.class_name)
|
||||
.iter()
|
||||
.find_map(|&class_name| {
|
||||
methods
|
||||
.get(class_name)
|
||||
.and_then(|class_methods| class_methods.get(method_name))
|
||||
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a property getter in the instance registry.
|
||||
|
||||
Returns `None` if the property getter is not found.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn find_property_getter<'lua>(
|
||||
lua: &'lua Lua,
|
||||
instance: &Instance,
|
||||
property_name: &str,
|
||||
) -> Option<LuaFunction<'lua>> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
let getters = registry
|
||||
.getters
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry getters");
|
||||
|
||||
class_name_chain(&instance.class_name)
|
||||
.iter()
|
||||
.find_map(|&class_name| {
|
||||
getters
|
||||
.get(class_name)
|
||||
.and_then(|class_getters| class_getters.get(property_name))
|
||||
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a property setter in the instance registry.
|
||||
|
||||
Returns `None` if the property setter is not found.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn find_property_setter<'lua>(
|
||||
lua: &'lua Lua,
|
||||
instance: &Instance,
|
||||
property_name: &str,
|
||||
) -> Option<LuaFunction<'lua>> {
|
||||
let registry = Self::get_or_create(lua);
|
||||
let setters = registry
|
||||
.setters
|
||||
.lock()
|
||||
.expect("Failed to lock instance registry setters");
|
||||
|
||||
class_name_chain(&instance.class_name)
|
||||
.iter()
|
||||
.find_map(|&class_name| {
|
||||
setters
|
||||
.get(class_name)
|
||||
.and_then(|class_setters| class_setters.get(property_name))
|
||||
.map(|key| lua.registry_value::<LuaFunction>(key).unwrap())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the class name chain for a given class name.
|
||||
|
||||
The chain starts with the given class name and ends with the root class.
|
||||
|
||||
# Panics
|
||||
|
||||
Panics if the class name is not valid.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn class_name_chain(class_name: &str) -> Vec<&str> {
|
||||
let db = rbx_reflection_database::get();
|
||||
|
||||
let mut list = vec![class_name];
|
||||
let mut current_name = class_name;
|
||||
|
||||
loop {
|
||||
let class_descriptor = db
|
||||
.classes
|
||||
.get(current_name)
|
||||
.expect("Got invalid class name");
|
||||
if let Some(sup) = &class_descriptor.superclass {
|
||||
current_name = sup.borrow();
|
||||
list.push(current_name);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
list
|
||||
}
|
93
crates/lune-roblox/src/instance/terrain.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
use mlua::prelude::*;
|
||||
use rbx_dom_weak::types::{MaterialColors, TerrainMaterials, Variant};
|
||||
|
||||
use crate::{
|
||||
datatypes::types::{Color3, EnumItem},
|
||||
shared::classes::{add_class_restricted_method, add_class_restricted_method_mut},
|
||||
};
|
||||
|
||||
use super::Instance;
|
||||
|
||||
pub const CLASS_NAME: &str = "Terrain";
|
||||
|
||||
pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M) {
|
||||
add_class_restricted_method(
|
||||
methods,
|
||||
CLASS_NAME,
|
||||
"GetMaterialColor",
|
||||
terrain_get_material_color,
|
||||
);
|
||||
|
||||
add_class_restricted_method_mut(
|
||||
methods,
|
||||
CLASS_NAME,
|
||||
"SetMaterialColor",
|
||||
terrain_set_material_color,
|
||||
);
|
||||
}
|
||||
|
||||
fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
|
||||
if let Some(Variant::MaterialColors(inner)) = instance.get_property("MaterialColors") {
|
||||
inner
|
||||
} else {
|
||||
MaterialColors::default()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the color of the given terrain material.
|
||||
|
||||
### See Also
|
||||
* [`GetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#GetMaterialColor)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn terrain_get_material_color(_: &Lua, this: &Instance, material: EnumItem) -> LuaResult<Color3> {
|
||||
let material_colors = get_or_create_material_colors(this);
|
||||
|
||||
if &material.parent.desc.name != "Material" {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected Enum.Material, got Enum.{}",
|
||||
&material.parent.desc.name
|
||||
)));
|
||||
}
|
||||
|
||||
let terrain_material = material
|
||||
.name
|
||||
.parse::<TerrainMaterials>()
|
||||
.map_err(|err| LuaError::RuntimeError(err.to_string()))?;
|
||||
|
||||
Ok(material_colors.get_color(terrain_material).into())
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the color of the given terrain material.
|
||||
|
||||
### See Also
|
||||
* [`SetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#SetMaterialColor)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn terrain_set_material_color(
|
||||
_: &Lua,
|
||||
this: &mut Instance,
|
||||
args: (EnumItem, Color3),
|
||||
) -> LuaResult<()> {
|
||||
let mut material_colors = get_or_create_material_colors(this);
|
||||
let material = args.0;
|
||||
let color = args.1;
|
||||
|
||||
if &material.parent.desc.name != "Material" {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Expected Enum.Material, got Enum.{}",
|
||||
&material.parent.desc.name
|
||||
)));
|
||||
}
|
||||
|
||||
let terrain_material = material
|
||||
.name
|
||||
.parse::<TerrainMaterials>()
|
||||
.map_err(|err| LuaError::RuntimeError(err.to_string()))?;
|
||||
|
||||
material_colors.set_color(terrain_material, color.into());
|
||||
this.set_property("MaterialColors", Variant::MaterialColors(material_colors));
|
||||
Ok(())
|
||||
}
|
34
crates/lune-roblox/src/instance/workspace.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use crate::shared::classes::{add_class_restricted_getter, get_or_create_property_ref_instance};
|
||||
|
||||
use super::Instance;
|
||||
|
||||
pub const CLASS_NAME: &str = "Workspace";
|
||||
|
||||
pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
|
||||
add_class_restricted_getter(f, CLASS_NAME, "Terrain", workspace_get_terrain);
|
||||
add_class_restricted_getter(f, CLASS_NAME, "CurrentCamera", workspace_get_camera);
|
||||
}
|
||||
|
||||
/**
|
||||
Get the terrain parented under this workspace, or create it if it doesn't exist.
|
||||
|
||||
### See Also
|
||||
* [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn workspace_get_terrain(_: &Lua, this: &Instance) -> LuaResult<Instance> {
|
||||
get_or_create_property_ref_instance(this, "Terrain", "Terrain")
|
||||
}
|
||||
|
||||
/**
|
||||
Get the camera parented under this workspace, or create it if it doesn't exist.
|
||||
|
||||
### See Also
|
||||
* [`CurrentCamera`](https://create.roblox.com/docs/reference/engine/classes/Workspace#CurrentCamera)
|
||||
on the Roblox Developer Hub
|
||||
*/
|
||||
fn workspace_get_camera(_: &Lua, this: &Instance) -> LuaResult<Instance> {
|
||||
get_or_create_property_ref_instance(this, "CurrentCamera", "Camera")
|
||||
}
|
72
crates/lune-roblox/src/lib.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
pub mod datatypes;
|
||||
pub mod document;
|
||||
pub mod instance;
|
||||
pub mod reflection;
|
||||
|
||||
pub(crate) mod exports;
|
||||
pub(crate) mod shared;
|
||||
|
||||
use exports::export;
|
||||
|
||||
fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
|
||||
use datatypes::types::*;
|
||||
use instance::Instance;
|
||||
Ok(vec![
|
||||
// Datatypes
|
||||
export::<Axes>(lua)?,
|
||||
export::<BrickColor>(lua)?,
|
||||
export::<CFrame>(lua)?,
|
||||
export::<Color3>(lua)?,
|
||||
export::<ColorSequence>(lua)?,
|
||||
export::<ColorSequenceKeypoint>(lua)?,
|
||||
export::<Content>(lua)?,
|
||||
export::<Faces>(lua)?,
|
||||
export::<Font>(lua)?,
|
||||
export::<NumberRange>(lua)?,
|
||||
export::<NumberSequence>(lua)?,
|
||||
export::<NumberSequenceKeypoint>(lua)?,
|
||||
export::<PhysicalProperties>(lua)?,
|
||||
export::<Ray>(lua)?,
|
||||
export::<Rect>(lua)?,
|
||||
export::<UDim>(lua)?,
|
||||
export::<UDim2>(lua)?,
|
||||
export::<Region3>(lua)?,
|
||||
export::<Region3int16>(lua)?,
|
||||
export::<Vector2>(lua)?,
|
||||
export::<Vector2int16>(lua)?,
|
||||
export::<Vector3>(lua)?,
|
||||
export::<Vector3int16>(lua)?,
|
||||
// Classes
|
||||
export::<Instance>(lua)?,
|
||||
// Singletons
|
||||
("Enum", Enums.into_lua(lua)?),
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a table containing all the Roblox datatypes, classes, and singletons.
|
||||
|
||||
Note that this is not guaranteed to contain any value unless indexed directly,
|
||||
it may be optimized to use lazy initialization in the future.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory or when a value cannot be created.
|
||||
*/
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
// FUTURE: We can probably create these lazily as users
|
||||
// index the main exports (this return value) table and
|
||||
// save some memory and startup time. The full exports
|
||||
// table is quite big and probably won't get any smaller
|
||||
// since we impl all roblox constructors for each datatype.
|
||||
let exports = create_all_exports(lua)?;
|
||||
TableBuilder::new(lua)?
|
||||
.with_values(exports)?
|
||||
.build_readonly()
|
||||
}
|
155
crates/lune-roblox/src/reflection/class.rs
Normal file
|
@ -0,0 +1,155 @@
|
|||
use core::fmt;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_dom_weak::types::Variant as DomVariant;
|
||||
use rbx_reflection::{ClassDescriptor, DataType};
|
||||
|
||||
use super::{property::DatabaseProperty, utils::*};
|
||||
use crate::datatypes::{
|
||||
conversion::DomValueToLua, types::EnumItem, userdata_impl_eq, userdata_impl_to_string,
|
||||
};
|
||||
|
||||
type DbClass = &'static ClassDescriptor<'static>;
|
||||
|
||||
/**
|
||||
A wrapper for [`rbx_reflection::ClassDescriptor`] that
|
||||
also provides access to the class descriptor from lua.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DatabaseClass(DbClass);
|
||||
|
||||
impl DatabaseClass {
|
||||
pub(crate) fn new(inner: DbClass) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the name of this class.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_name(&self) -> String {
|
||||
self.0.name.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Get the superclass (parent class) of this class.
|
||||
|
||||
May be `None` if no parent class exists.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_superclass(&self) -> Option<String> {
|
||||
let sup = self.0.superclass.as_ref()?;
|
||||
Some(sup.to_string())
|
||||
}
|
||||
|
||||
/**
|
||||
Get all known properties for this class.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_properties(&self) -> HashMap<String, DatabaseProperty> {
|
||||
self.0
|
||||
.properties
|
||||
.iter()
|
||||
.map(|(name, prop)| (name.to_string(), DatabaseProperty::new(self.0, prop)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Get all default values for properties of this class.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_defaults(&self) -> HashMap<String, DomVariant> {
|
||||
self.0
|
||||
.default_properties
|
||||
.iter()
|
||||
.map(|(name, prop)| (name.to_string(), prop.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Get all tags describing the class.
|
||||
|
||||
These include information such as if the class can be replicated
|
||||
to players at runtime, and top-level class categories.
|
||||
*/
|
||||
pub fn get_tags_str(&self) -> Vec<&'static str> {
|
||||
self.0
|
||||
.tags
|
||||
.iter()
|
||||
.copied()
|
||||
.map(class_tag_to_str)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for DatabaseClass {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
|
||||
fields.add_field_method_get("Superclass", |_, this| Ok(this.get_superclass()));
|
||||
fields.add_field_method_get("Properties", |_, this| Ok(this.get_properties()));
|
||||
fields.add_field_method_get("DefaultProperties", |lua, this| {
|
||||
let defaults = this.get_defaults();
|
||||
let mut map = HashMap::with_capacity(defaults.len());
|
||||
for (name, prop) in defaults {
|
||||
let value = if let DomVariant::Enum(enum_value) = prop {
|
||||
make_enum_value(this.0, &name, enum_value.to_u32())
|
||||
.and_then(|e| e.into_lua(lua))
|
||||
} else {
|
||||
LuaValue::dom_value_to_lua(lua, &prop).into_lua_err()
|
||||
};
|
||||
if let Ok(value) = value {
|
||||
map.insert(name, value);
|
||||
}
|
||||
}
|
||||
Ok(map)
|
||||
});
|
||||
fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str()));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DatabaseClass {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.name == other.0.name
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseClass {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ReflectionDatabaseClass({})", self.0.name)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_enum_name(inner: DbClass, name: impl AsRef<str>) -> Option<String> {
|
||||
inner.properties.iter().find_map(|(prop_name, prop_info)| {
|
||||
if prop_name == name.as_ref() {
|
||||
if let DataType::Enum(enum_name) = &prop_info.data_type {
|
||||
Some(enum_name.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn make_enum_value(inner: DbClass, name: impl AsRef<str>, value: u32) -> LuaResult<EnumItem> {
|
||||
let name = name.as_ref();
|
||||
let enum_name = find_enum_name(inner, name).ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get default property '{name}' - No enum descriptor was found",
|
||||
))
|
||||
})?;
|
||||
EnumItem::from_enum_name_and_value(&enum_name, value).ok_or_else(|| {
|
||||
LuaError::RuntimeError(format!(
|
||||
"Failed to get default property '{name}' - Enum.{enum_name} does not contain numeric value {value}",
|
||||
))
|
||||
})
|
||||
}
|
69
crates/lune-roblox/src/reflection/enums.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use std::{collections::HashMap, fmt};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_reflection::EnumDescriptor;
|
||||
|
||||
use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string};
|
||||
|
||||
type DbEnum = &'static EnumDescriptor<'static>;
|
||||
|
||||
/**
|
||||
A wrapper for [`rbx_reflection::EnumDescriptor`] that
|
||||
also provides access to the class descriptor from lua.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DatabaseEnum(DbEnum);
|
||||
|
||||
impl DatabaseEnum {
|
||||
pub(crate) fn new(inner: DbEnum) -> Self {
|
||||
Self(inner)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the name of this enum.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_name(&self) -> String {
|
||||
self.0.name.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Get all known members of this enum.
|
||||
|
||||
Note that this is a direct map of name -> enum values,
|
||||
and does not actually use the `EnumItem` datatype itself.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_items(&self) -> HashMap<String, u32> {
|
||||
self.0
|
||||
.items
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), *v))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for DatabaseEnum {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
|
||||
fields.add_field_method_get("Items", |_, this| Ok(this.get_items()));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DatabaseEnum {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.name == other.0.name
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseEnum {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ReflectionDatabaseEnum({})", self.0.name)
|
||||
}
|
||||
}
|
151
crates/lune-roblox/src/reflection/mod.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
use std::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_reflection::ReflectionDatabase;
|
||||
|
||||
use crate::datatypes::userdata_impl_eq;
|
||||
|
||||
mod class;
|
||||
mod enums;
|
||||
mod property;
|
||||
mod utils;
|
||||
|
||||
pub use class::DatabaseClass;
|
||||
pub use enums::DatabaseEnum;
|
||||
pub use property::DatabaseProperty;
|
||||
|
||||
use super::datatypes::userdata_impl_to_string;
|
||||
|
||||
type Db = &'static ReflectionDatabase<'static>;
|
||||
|
||||
/**
|
||||
A wrapper for [`rbx_reflection::ReflectionDatabase`] that
|
||||
also provides access to the reflection database from lua.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Database(Db);
|
||||
|
||||
impl Database {
|
||||
/**
|
||||
Creates a new database struct, referencing the bundled reflection database.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/**
|
||||
Get the version string of the database.
|
||||
|
||||
This will follow the format `x.y.z.w`, which most
|
||||
commonly looks something like `0.567.0.123456789`.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_version(&self) -> String {
|
||||
let [x, y, z, w] = self.0.version;
|
||||
format!("{x}.{y}.{z}.{w}")
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves a list of all currently known enum names.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_enum_names(&self) -> Vec<String> {
|
||||
self.0.enums.keys().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Retrieves a list of all currently known class names.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_class_names(&self) -> Vec<String> {
|
||||
self.0.classes.keys().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
/**
|
||||
Gets an enum with the exact given name, if one exists.
|
||||
*/
|
||||
pub fn get_enum(&self, name: impl AsRef<str>) -> Option<DatabaseEnum> {
|
||||
let e = self.0.enums.get(name.as_ref())?;
|
||||
Some(DatabaseEnum::new(e))
|
||||
}
|
||||
|
||||
/**
|
||||
Gets a class with the exact given name, if one exists.
|
||||
*/
|
||||
pub fn get_class(&self, name: impl AsRef<str>) -> Option<DatabaseClass> {
|
||||
let c = self.0.classes.get(name.as_ref())?;
|
||||
Some(DatabaseClass::new(c))
|
||||
}
|
||||
|
||||
/**
|
||||
Finds an enum with the given name.
|
||||
|
||||
This will use case-insensitive matching and ignore leading and trailing whitespace.
|
||||
*/
|
||||
pub fn find_enum(&self, name: impl AsRef<str>) -> Option<DatabaseEnum> {
|
||||
let name = name.as_ref().trim().to_lowercase();
|
||||
let (ename, _) = self
|
||||
.0
|
||||
.enums
|
||||
.iter()
|
||||
.find(|(ename, _)| ename.trim().to_lowercase() == name)?;
|
||||
self.get_enum(ename)
|
||||
}
|
||||
|
||||
/**
|
||||
Finds a class with the given name.
|
||||
|
||||
This will use case-insensitive matching and ignore leading and trailing whitespace.
|
||||
*/
|
||||
pub fn find_class(&self, name: impl AsRef<str>) -> Option<DatabaseClass> {
|
||||
let name = name.as_ref().trim().to_lowercase();
|
||||
let (cname, _) = self
|
||||
.0
|
||||
.classes
|
||||
.iter()
|
||||
.find(|(cname, _)| cname.trim().to_lowercase() == name)?;
|
||||
self.get_class(cname)
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for Database {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Version", |_, this| Ok(this.get_version()));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
methods.add_method("GetEnumNames", |_, this, (): ()| Ok(this.get_enum_names()));
|
||||
methods.add_method(
|
||||
"GetClassNames",
|
||||
|_, this, (): ()| Ok(this.get_class_names()),
|
||||
);
|
||||
methods.add_method("GetEnum", |_, this, name: String| Ok(this.get_enum(name)));
|
||||
methods.add_method("GetClass", |_, this, name: String| Ok(this.get_class(name)));
|
||||
methods.add_method("FindEnum", |_, this, name: String| Ok(this.find_enum(name)));
|
||||
methods.add_method("FindClass", |_, this, name: String| {
|
||||
Ok(this.find_class(name))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Database {
|
||||
fn default() -> Self {
|
||||
Self(rbx_reflection_database::get())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Database {
|
||||
fn eq(&self, _other: &Self) -> bool {
|
||||
true // All database userdatas refer to the same underlying rbx-dom database
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Database {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ReflectionDatabase")
|
||||
}
|
||||
}
|
99
crates/lune-roblox/src/reflection/property.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
use std::fmt;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use rbx_reflection::{ClassDescriptor, PropertyDescriptor};
|
||||
|
||||
use super::utils::*;
|
||||
use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string};
|
||||
|
||||
type DbClass = &'static ClassDescriptor<'static>;
|
||||
type DbProp = &'static PropertyDescriptor<'static>;
|
||||
|
||||
/**
|
||||
A wrapper for [`rbx_reflection::PropertyDescriptor`] that
|
||||
also provides access to the property descriptor from lua.
|
||||
*/
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DatabaseProperty(DbClass, DbProp);
|
||||
|
||||
impl DatabaseProperty {
|
||||
pub(crate) fn new(inner: DbClass, inner_prop: DbProp) -> Self {
|
||||
Self(inner, inner_prop)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the name of this property.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_name(&self) -> String {
|
||||
self.1.name.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Get the datatype name of the property.
|
||||
|
||||
For normal datatypes this will be a string such as `string`, `Color3`, ...
|
||||
|
||||
For enums this will be a string formatted as `Enum.EnumName`.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_datatype_name(&self) -> String {
|
||||
data_type_to_str(self.1.data_type.clone())
|
||||
}
|
||||
|
||||
/**
|
||||
Get the scriptability of this property, meaning if it can be written / read at runtime.
|
||||
|
||||
All properties are writable and readable in Lune even if scriptability is not.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn get_scriptability_str(&self) -> &'static str {
|
||||
scriptability_to_str(self.1.scriptability)
|
||||
}
|
||||
|
||||
/**
|
||||
Get all known tags describing the property.
|
||||
|
||||
These include information such as if the property can be replicated to players
|
||||
at runtime, if the property should be hidden in Roblox Studio, and more.
|
||||
*/
|
||||
pub fn get_tags_str(&self) -> Vec<&'static str> {
|
||||
self.1
|
||||
.tags
|
||||
.iter()
|
||||
.copied()
|
||||
.map(property_tag_to_str)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for DatabaseProperty {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("Name", |_, this| Ok(this.get_name()));
|
||||
fields.add_field_method_get("Datatype", |_, this| Ok(this.get_datatype_name()));
|
||||
fields.add_field_method_get("Scriptability", |_, this| Ok(this.get_scriptability_str()));
|
||||
fields.add_field_method_get("Tags", |_, this| Ok(this.get_tags_str()));
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
|
||||
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DatabaseProperty {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.name == other.0.name && self.1.name == other.1.name
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabaseProperty {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"ReflectionDatabaseProperty({} > {})",
|
||||
self.0.name, self.1.name
|
||||
)
|
||||
}
|
||||
}
|
56
crates/lune-roblox/src/reflection/utils.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use rbx_reflection::{ClassTag, DataType, PropertyTag, Scriptability};
|
||||
|
||||
use crate::datatypes::extension::DomValueExt;
|
||||
|
||||
pub fn data_type_to_str(data_type: DataType) -> String {
|
||||
match data_type {
|
||||
DataType::Enum(e) => format!("Enum.{e}"),
|
||||
DataType::Value(v) => v
|
||||
.variant_name()
|
||||
.expect("Encountered unknown data type variant")
|
||||
.to_string(),
|
||||
_ => panic!("Encountered unknown data type"),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
NOTE: Remember to add any new strings here to typedefs too!
|
||||
*/
|
||||
|
||||
pub fn scriptability_to_str(scriptability: Scriptability) -> &'static str {
|
||||
match scriptability {
|
||||
Scriptability::None => "None",
|
||||
Scriptability::Custom => "Custom",
|
||||
Scriptability::Read => "Read",
|
||||
Scriptability::ReadWrite => "ReadWrite",
|
||||
Scriptability::Write => "Write",
|
||||
_ => panic!("Encountered unknown scriptability"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn property_tag_to_str(tag: PropertyTag) -> &'static str {
|
||||
match tag {
|
||||
PropertyTag::Deprecated => "Deprecated",
|
||||
PropertyTag::Hidden => "Hidden",
|
||||
PropertyTag::NotBrowsable => "NotBrowsable",
|
||||
PropertyTag::NotReplicated => "NotReplicated",
|
||||
PropertyTag::NotScriptable => "NotScriptable",
|
||||
PropertyTag::ReadOnly => "ReadOnly",
|
||||
PropertyTag::WriteOnly => "WriteOnly",
|
||||
_ => panic!("Encountered unknown property tag"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn class_tag_to_str(tag: ClassTag) -> &'static str {
|
||||
match tag {
|
||||
ClassTag::Deprecated => "Deprecated",
|
||||
ClassTag::NotBrowsable => "NotBrowsable",
|
||||
ClassTag::NotCreatable => "NotCreatable",
|
||||
ClassTag::NotReplicated => "NotReplicated",
|
||||
ClassTag::PlayerReplicated => "PlayerReplicated",
|
||||
ClassTag::Service => "Service",
|
||||
ClassTag::Settings => "Settings",
|
||||
ClassTag::UserSettings => "UserSettings",
|
||||
_ => panic!("Encountered unknown class tag"),
|
||||
}
|
||||
}
|
129
crates/lune-roblox/src/shared/classes.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use rbx_dom_weak::types::Variant as DomValue;
|
||||
|
||||
use crate::instance::Instance;
|
||||
|
||||
use super::instance::class_is_a;
|
||||
|
||||
pub(crate) fn add_class_restricted_getter<'lua, F: LuaUserDataFields<'lua, Instance>, R, G>(
|
||||
fields: &mut F,
|
||||
class_name: &'static str,
|
||||
field_name: &'static str,
|
||||
field_getter: G,
|
||||
) where
|
||||
R: IntoLua<'lua>,
|
||||
G: 'static + Fn(&'lua Lua, &Instance) -> LuaResult<R>,
|
||||
{
|
||||
fields.add_field_method_get(field_name, move |lua, this| {
|
||||
if class_is_a(this.get_class_name(), class_name).unwrap_or(false) {
|
||||
field_getter(lua, this)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{field_name} is not a valid member of {class_name}",
|
||||
)))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn add_class_restricted_setter<'lua, F: LuaUserDataFields<'lua, Instance>, A, G>(
|
||||
fields: &mut F,
|
||||
class_name: &'static str,
|
||||
field_name: &'static str,
|
||||
field_getter: G,
|
||||
) where
|
||||
A: FromLua<'lua>,
|
||||
G: 'static + Fn(&'lua Lua, &Instance, A) -> LuaResult<()>,
|
||||
{
|
||||
fields.add_field_method_set(field_name, move |lua, this, value| {
|
||||
if class_is_a(this.get_class_name(), class_name).unwrap_or(false) {
|
||||
field_getter(lua, this, value)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{field_name} is not a valid member of {class_name}",
|
||||
)))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn add_class_restricted_method<'lua, M: LuaUserDataMethods<'lua, Instance>, A, R, F>(
|
||||
methods: &mut M,
|
||||
class_name: &'static str,
|
||||
method_name: &'static str,
|
||||
method: F,
|
||||
) where
|
||||
A: FromLuaMulti<'lua>,
|
||||
R: IntoLuaMulti<'lua>,
|
||||
F: 'static + Fn(&'lua Lua, &Instance, A) -> LuaResult<R>,
|
||||
{
|
||||
methods.add_method(method_name, move |lua, this, args| {
|
||||
if class_is_a(this.get_class_name(), class_name).unwrap_or(false) {
|
||||
method(lua, this, args)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{method_name} is not a valid member of {class_name}",
|
||||
)))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn add_class_restricted_method_mut<
|
||||
'lua,
|
||||
M: LuaUserDataMethods<'lua, Instance>,
|
||||
A,
|
||||
R,
|
||||
F,
|
||||
>(
|
||||
methods: &mut M,
|
||||
class_name: &'static str,
|
||||
method_name: &'static str,
|
||||
method: F,
|
||||
) where
|
||||
A: FromLuaMulti<'lua>,
|
||||
R: IntoLuaMulti<'lua>,
|
||||
F: 'static + Fn(&'lua Lua, &mut Instance, A) -> LuaResult<R>,
|
||||
{
|
||||
methods.add_method_mut(method_name, move |lua, this, args| {
|
||||
if class_is_a(this.get_class_name(), class_name).unwrap_or(false) {
|
||||
method(lua, this, args)
|
||||
} else {
|
||||
Err(LuaError::RuntimeError(format!(
|
||||
"{method_name} is not a valid member of {class_name}",
|
||||
)))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
Gets or creates the instance child with the given reference prop name and class name.
|
||||
|
||||
Note that the class name here must be an exact match, it is not checked using `IsA`.
|
||||
|
||||
The instance may be in one of several states but this function will guarantee that the
|
||||
property reference is correct and that the instance exists after it has been called:
|
||||
|
||||
1. Instance exists as property ref - just return it
|
||||
2. Instance exists under workspace but not as a property ref - save it and return it
|
||||
3. Instance does not exist - create it, save it, then return it
|
||||
*/
|
||||
pub(crate) fn get_or_create_property_ref_instance(
|
||||
this: &Instance,
|
||||
prop_name: &'static str,
|
||||
class_name: &'static str,
|
||||
) -> LuaResult<Instance> {
|
||||
if let Some(DomValue::Ref(inst_ref)) = this.get_property(prop_name) {
|
||||
if let Some(inst) = Instance::new_opt(inst_ref) {
|
||||
return Ok(inst);
|
||||
}
|
||||
}
|
||||
if let Some(inst) = this.find_child(|child| child.class == class_name) {
|
||||
this.set_property(prop_name, DomValue::Ref(inst.dom_ref));
|
||||
Ok(inst)
|
||||
} else {
|
||||
let inst = Instance::new_orphaned(class_name);
|
||||
inst.set_parent(Some(*this));
|
||||
this.set_property(prop_name, DomValue::Ref(inst.dom_ref));
|
||||
Ok(inst)
|
||||
}
|
||||
}
|
209
crates/lune-roblox/src/shared/instance.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
use std::borrow::{Borrow, BorrowMut, Cow};
|
||||
|
||||
use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
|
||||
use rbx_reflection::{ClassTag, DataType};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct PropertyInfo {
|
||||
pub enum_name: Option<Cow<'static, str>>,
|
||||
pub enum_default: Option<u32>,
|
||||
pub value_type: Option<DomType>,
|
||||
pub value_default: Option<&'static DomValue>,
|
||||
}
|
||||
|
||||
/**
|
||||
Finds the info of a property of the given class.
|
||||
|
||||
This will also check superclasses if the property
|
||||
was not directly found for the given class.
|
||||
|
||||
Returns `None` if the class or property does not exist.
|
||||
*/
|
||||
pub(crate) fn find_property_info(
|
||||
instance_class: impl AsRef<str>,
|
||||
property_name: impl AsRef<str>,
|
||||
) -> Option<PropertyInfo> {
|
||||
let db = rbx_reflection_database::get();
|
||||
|
||||
let instance_class = instance_class.as_ref();
|
||||
let property_name = property_name.as_ref();
|
||||
|
||||
// Attributes and tags are *technically* properties but we don't
|
||||
// want to treat them as such when looking up property info, any
|
||||
// reading or modification of these should always be explicit
|
||||
if matches!(property_name, "Attributes" | "Tags") {
|
||||
return None;
|
||||
}
|
||||
|
||||
// FUTURE: We can probably cache the result of calling this
|
||||
// function, if property access is being used in a tight loop
|
||||
// in a build step or something similar then it would be beneficial
|
||||
|
||||
let mut class_name = Cow::Borrowed(instance_class);
|
||||
let mut class_info = None;
|
||||
|
||||
while let Some(class) = db.classes.get(class_name.as_ref()) {
|
||||
if let Some(prop_definition) = class.properties.get(property_name) {
|
||||
/*
|
||||
We found a property, create a property info containing name/type
|
||||
|
||||
Note that we might have found the property in the
|
||||
base class but the default value can be part of
|
||||
some separate class, it will be checked below
|
||||
*/
|
||||
class_info = Some(match &prop_definition.data_type {
|
||||
DataType::Enum(enum_name) => PropertyInfo {
|
||||
enum_name: Some(Cow::Borrowed(enum_name)),
|
||||
..Default::default()
|
||||
},
|
||||
DataType::Value(value_type) => PropertyInfo {
|
||||
value_type: Some(*value_type),
|
||||
..Default::default()
|
||||
},
|
||||
_ => PropertyInfo::default(),
|
||||
});
|
||||
break;
|
||||
} else if let Some(sup) = &class.superclass {
|
||||
// No property found, we should look at the superclass
|
||||
class_name = Cow::Borrowed(sup);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(class_info) = class_info.borrow_mut() {
|
||||
class_name = Cow::Borrowed(instance_class);
|
||||
while let Some(class) = db.classes.get(class_name.as_ref()) {
|
||||
if let Some(default) = class.default_properties.get(property_name) {
|
||||
// We found a default value, map it to a more useful value for us
|
||||
if class_info.enum_name.is_some() {
|
||||
class_info.enum_default = match default {
|
||||
DomValue::Enum(enum_default) => Some(enum_default.to_u32()),
|
||||
_ => None,
|
||||
};
|
||||
} else if class_info.value_type.is_some() {
|
||||
class_info.value_default = Some(default);
|
||||
}
|
||||
break;
|
||||
} else if let Some(sup) = &class.superclass {
|
||||
// No default value found, we should look at the superclass
|
||||
class_name = Cow::Borrowed(sup);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class_info
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if an instance class exists in the reflection database.
|
||||
*/
|
||||
pub fn class_exists(class_name: impl AsRef<str>) -> bool {
|
||||
let db = rbx_reflection_database::get();
|
||||
db.classes.contains_key(class_name.as_ref())
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if an instance class matches a given class or superclass, similar to
|
||||
[Instance::IsA](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
|
||||
from the Roblox standard library.
|
||||
|
||||
Note that this function may return `None` if it encounters a class or superclass
|
||||
that does not exist in the currently known class reflection database.
|
||||
*/
|
||||
pub fn class_is_a(instance_class: impl AsRef<str>, class_name: impl AsRef<str>) -> Option<bool> {
|
||||
let mut instance_class = instance_class.as_ref();
|
||||
let class_name = class_name.as_ref();
|
||||
|
||||
if class_name == "Instance" || instance_class == class_name {
|
||||
Some(true)
|
||||
} else {
|
||||
let db = rbx_reflection_database::get();
|
||||
|
||||
while instance_class != class_name {
|
||||
let class_descriptor = db.classes.get(instance_class)?;
|
||||
if let Some(sup) = &class_descriptor.superclass {
|
||||
instance_class = sup.borrow();
|
||||
} else {
|
||||
return Some(false);
|
||||
}
|
||||
}
|
||||
|
||||
Some(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Checks if an instance class is a service.
|
||||
|
||||
This is separate from [`class_is_a`] since services do not share a
|
||||
common base class, and are instead determined through reflection tags.
|
||||
|
||||
Note that this function may return `None` if it encounters a class or superclass
|
||||
that does not exist in the currently known class reflection database.
|
||||
*/
|
||||
pub fn class_is_a_service(instance_class: impl AsRef<str>) -> Option<bool> {
|
||||
let mut instance_class = instance_class.as_ref();
|
||||
|
||||
let db = rbx_reflection_database::get();
|
||||
|
||||
loop {
|
||||
let class_descriptor = db.classes.get(instance_class)?;
|
||||
if class_descriptor.tags.contains(&ClassTag::Service) {
|
||||
return Some(true);
|
||||
} else if let Some(sup) = &class_descriptor.superclass {
|
||||
instance_class = sup.borrow();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Some(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_a_class_valid() {
|
||||
assert_eq!(class_is_a("Part", "Part"), Some(true));
|
||||
assert_eq!(class_is_a("Part", "BasePart"), Some(true));
|
||||
assert_eq!(class_is_a("Part", "PVInstance"), Some(true));
|
||||
assert_eq!(class_is_a("Part", "Instance"), Some(true));
|
||||
|
||||
assert_eq!(class_is_a("Workspace", "Workspace"), Some(true));
|
||||
assert_eq!(class_is_a("Workspace", "Model"), Some(true));
|
||||
assert_eq!(class_is_a("Workspace", "Instance"), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_a_class_invalid() {
|
||||
assert_eq!(class_is_a("Part", "part"), Some(false));
|
||||
assert_eq!(class_is_a("Part", "Base-Part"), Some(false));
|
||||
assert_eq!(class_is_a("Part", "Model"), Some(false));
|
||||
assert_eq!(class_is_a("Part", "Paart"), Some(false));
|
||||
|
||||
assert_eq!(class_is_a("Workspace", "Service"), Some(false));
|
||||
assert_eq!(class_is_a("Workspace", "."), Some(false));
|
||||
assert_eq!(class_is_a("Workspace", ""), Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_a_service_valid() {
|
||||
assert_eq!(class_is_a_service("Workspace"), Some(true));
|
||||
assert_eq!(class_is_a_service("PhysicsService"), Some(true));
|
||||
assert_eq!(class_is_a_service("ReplicatedFirst"), Some(true));
|
||||
assert_eq!(class_is_a_service("CSGDictionaryService"), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_a_service_invalid() {
|
||||
assert_eq!(class_is_a_service("Camera"), Some(false));
|
||||
assert_eq!(class_is_a_service("Terrain"), Some(false));
|
||||
assert_eq!(class_is_a_service("Work-space"), None);
|
||||
assert_eq!(class_is_a_service("CSG Dictionary Service"), None);
|
||||
}
|
||||
}
|
3
crates/lune-roblox/src/shared/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub(crate) mod classes;
|
||||
pub(crate) mod instance;
|
||||
pub(crate) mod userdata;
|
206
crates/lune-roblox/src/shared/userdata.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
#![allow(clippy::missing_errors_doc)]
|
||||
|
||||
use std::{any::type_name, cell::RefCell, fmt, ops};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
// Utility functions
|
||||
|
||||
type ListWriter = dyn Fn(&mut fmt::Formatter<'_>, bool, &str) -> fmt::Result;
|
||||
|
||||
#[must_use]
|
||||
pub fn make_list_writer() -> Box<ListWriter> {
|
||||
let first = RefCell::new(true);
|
||||
Box::new(move |f, flag, literal| {
|
||||
if flag {
|
||||
if first.take() {
|
||||
write!(f, "{literal}")?;
|
||||
} else {
|
||||
write!(f, ", {literal}")?;
|
||||
}
|
||||
}
|
||||
Ok::<_, fmt::Error>(())
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Userdata metamethod implementations
|
||||
|
||||
Note that many of these return [`LuaResult`] even though they don't
|
||||
return any errors - this is for consistency reasons and to make it
|
||||
easier to add these blanket implementations to [`LuaUserData`] impls.
|
||||
*/
|
||||
|
||||
pub fn userdata_impl_to_string<D>(_: &Lua, datatype: &D, _: ()) -> LuaResult<String>
|
||||
where
|
||||
D: LuaUserData + ToString + 'static,
|
||||
{
|
||||
Ok(datatype.to_string())
|
||||
}
|
||||
|
||||
pub fn userdata_impl_eq<D>(_: &Lua, datatype: &D, value: LuaValue) -> LuaResult<bool>
|
||||
where
|
||||
D: LuaUserData + PartialEq + 'static,
|
||||
{
|
||||
if let LuaValue::UserData(ud) = value {
|
||||
if let Ok(value_as_datatype) = ud.borrow::<D>() {
|
||||
Ok(*datatype == *value_as_datatype)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn userdata_impl_unm<D>(_: &Lua, datatype: &D, _: ()) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + ops::Neg<Output = D> + Copy,
|
||||
{
|
||||
Ok(-*datatype)
|
||||
}
|
||||
|
||||
pub fn userdata_impl_add<D>(_: &Lua, datatype: &D, value: LuaUserDataRef<D>) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + ops::Add<Output = D> + Copy,
|
||||
{
|
||||
Ok(*datatype + *value)
|
||||
}
|
||||
|
||||
pub fn userdata_impl_sub<D>(_: &Lua, datatype: &D, value: LuaUserDataRef<D>) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + ops::Sub<Output = D> + Copy,
|
||||
{
|
||||
Ok(*datatype - *value)
|
||||
}
|
||||
|
||||
pub fn userdata_impl_mul_f32<D>(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + ops::Mul<D, Output = D> + ops::Mul<f32, Output = D> + Copy + 'static,
|
||||
{
|
||||
match &rhs {
|
||||
LuaValue::Number(n) => return Ok(*datatype * *n as f32),
|
||||
LuaValue::Integer(i) => return Ok(*datatype * *i as f32),
|
||||
LuaValue::UserData(ud) => {
|
||||
if let Ok(vec) = ud.borrow::<D>() {
|
||||
return Ok(*datatype * *vec);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: rhs.type_name(),
|
||||
to: type_name::<D>(),
|
||||
message: Some(format!(
|
||||
"Expected {} or number, got {}",
|
||||
type_name::<D>(),
|
||||
rhs.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn userdata_impl_mul_i32<D>(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + ops::Mul<D, Output = D> + ops::Mul<i32, Output = D> + Copy + 'static,
|
||||
{
|
||||
match &rhs {
|
||||
LuaValue::Number(n) => return Ok(*datatype * *n as i32),
|
||||
LuaValue::Integer(i) => return Ok(*datatype * *i),
|
||||
LuaValue::UserData(ud) => {
|
||||
if let Ok(vec) = ud.borrow::<D>() {
|
||||
return Ok(*datatype * *vec);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: rhs.type_name(),
|
||||
to: type_name::<D>(),
|
||||
message: Some(format!(
|
||||
"Expected {} or number, got {}",
|
||||
type_name::<D>(),
|
||||
rhs.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn userdata_impl_div_f32<D>(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + ops::Div<D, Output = D> + ops::Div<f32, Output = D> + Copy + 'static,
|
||||
{
|
||||
match &rhs {
|
||||
LuaValue::Number(n) => return Ok(*datatype / *n as f32),
|
||||
LuaValue::Integer(i) => return Ok(*datatype / *i as f32),
|
||||
LuaValue::UserData(ud) => {
|
||||
if let Ok(vec) = ud.borrow::<D>() {
|
||||
return Ok(*datatype / *vec);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: rhs.type_name(),
|
||||
to: type_name::<D>(),
|
||||
message: Some(format!(
|
||||
"Expected {} or number, got {}",
|
||||
type_name::<D>(),
|
||||
rhs.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
pub trait IDiv<Rhs = Self> {
|
||||
type Output;
|
||||
#[must_use]
|
||||
fn idiv(self, rhs: Rhs) -> Self::Output;
|
||||
}
|
||||
|
||||
pub fn userdata_impl_idiv_f32<D>(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + IDiv<D, Output = D> + IDiv<f32, Output = D> + Copy + 'static,
|
||||
{
|
||||
match &rhs {
|
||||
LuaValue::Number(n) => return Ok(datatype.idiv(*n as f32)),
|
||||
LuaValue::Integer(i) => return Ok(datatype.idiv(*i as f32)),
|
||||
LuaValue::UserData(ud) => {
|
||||
if let Ok(vec) = ud.borrow::<D>() {
|
||||
return Ok(datatype.idiv(*vec));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: rhs.type_name(),
|
||||
to: type_name::<D>(),
|
||||
message: Some(format!(
|
||||
"Expected {} or number, got {}",
|
||||
type_name::<D>(),
|
||||
rhs.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn userdata_impl_div_i32<D>(_: &Lua, datatype: &D, rhs: LuaValue) -> LuaResult<D>
|
||||
where
|
||||
D: LuaUserData + ops::Div<D, Output = D> + ops::Div<i32, Output = D> + Copy + 'static,
|
||||
{
|
||||
match &rhs {
|
||||
LuaValue::Number(n) => return Ok(*datatype / *n as i32),
|
||||
LuaValue::Integer(i) => return Ok(*datatype / *i),
|
||||
LuaValue::UserData(ud) => {
|
||||
if let Ok(vec) = ud.borrow::<D>() {
|
||||
return Ok(*datatype / *vec);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Err(LuaError::FromLuaConversionError {
|
||||
from: rhs.type_name(),
|
||||
to: type_name::<D>(),
|
||||
message: Some(format!(
|
||||
"Expected {} or number, got {}",
|
||||
type_name::<D>(),
|
||||
rhs.type_name()
|
||||
)),
|
||||
})
|
||||
}
|
22
crates/lune-std-datetime/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "lune-std-datetime"
|
||||
version = "0.1.3"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - DateTime"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.9.9", features = ["luau"] }
|
||||
|
||||
thiserror = "1.0"
|
||||
chrono = "0.4.38"
|
||||
chrono_lc = "0.1.6"
|
||||
|
||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
250
crates/lune-std-datetime/src/date_time.rs
Normal file
|
@ -0,0 +1,250 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use chrono::DateTime as ChronoDateTime;
|
||||
use chrono_lc::LocaleDate;
|
||||
|
||||
use crate::result::{DateTimeError, DateTimeResult};
|
||||
use crate::values::DateTimeValues;
|
||||
|
||||
const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
|
||||
const DEFAULT_LOCALE: &str = "en";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct DateTime {
|
||||
// NOTE: We store this as the UTC time zone since it is the most commonly
|
||||
// used and getting the generics right for TimeZone is somewhat tricky,
|
||||
// but none of the method implementations below should rely on this tz
|
||||
inner: ChronoDateTime<Utc>,
|
||||
}
|
||||
|
||||
impl DateTime {
|
||||
/**
|
||||
Creates a new `DateTime` struct representing the current moment in time.
|
||||
|
||||
See [`chrono::DateTime::now`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn now() -> Self {
|
||||
Self { inner: Utc::now() }
|
||||
}
|
||||
|
||||
/**
|
||||
Creates a new `DateTime` struct from the given `unix_timestamp`,
|
||||
which is a float of seconds passed since the UNIX epoch.
|
||||
|
||||
This is somewhat unconventional, but fits our Luau interface and dynamic types quite well.
|
||||
To use this method the same way you would use a more traditional `from_unix_timestamp`
|
||||
that takes a `u64` of seconds or similar type, casting the value is sufficient:
|
||||
|
||||
```rust ignore
|
||||
DateTime::from_unix_timestamp_float(123456789u64 as f64)
|
||||
```
|
||||
|
||||
See [`chrono::DateTime::from_timestamp`] for additional details.
|
||||
|
||||
# Errors
|
||||
|
||||
Returns an error if the input value is out of range.
|
||||
*/
|
||||
pub fn from_unix_timestamp_float(unix_timestamp: f64) -> DateTimeResult<Self> {
|
||||
let whole = unix_timestamp.trunc() as i64;
|
||||
let fract = unix_timestamp.fract();
|
||||
let nanos = (fract * 1_000_000_000f64)
|
||||
.round()
|
||||
.clamp(u32::MIN as f64, u32::MAX as f64) as u32;
|
||||
let inner = ChronoDateTime::<Utc>::from_timestamp(whole, nanos)
|
||||
.ok_or(DateTimeError::OutOfRangeUnspecified)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Transforms individual date & time values into a new
|
||||
`DateTime` struct, using the universal (UTC) time zone.
|
||||
|
||||
See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
|
||||
for additional details and cases where this constructor may return an error.
|
||||
|
||||
# Errors
|
||||
|
||||
Returns an error if the date or time values are invalid.
|
||||
*/
|
||||
pub fn from_universal_time(values: &DateTimeValues) -> DateTimeResult<Self> {
|
||||
let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
|
||||
.ok_or(DateTimeError::InvalidDate)?;
|
||||
|
||||
let time = NaiveTime::from_hms_milli_opt(
|
||||
values.hour,
|
||||
values.minute,
|
||||
values.second,
|
||||
values.millisecond,
|
||||
)
|
||||
.ok_or(DateTimeError::InvalidTime)?;
|
||||
|
||||
let inner = Utc.from_utc_datetime(&NaiveDateTime::new(date, time));
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Transforms individual date & time values into a new
|
||||
`DateTime` struct, using the current local time zone.
|
||||
|
||||
See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
|
||||
for additional details and cases where this constructor may return an error.
|
||||
|
||||
# Errors
|
||||
|
||||
Returns an error if the date or time values are invalid or ambiguous.
|
||||
*/
|
||||
pub fn from_local_time(values: &DateTimeValues) -> DateTimeResult<Self> {
|
||||
let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
|
||||
.ok_or(DateTimeError::InvalidDate)?;
|
||||
|
||||
let time = NaiveTime::from_hms_milli_opt(
|
||||
values.hour,
|
||||
values.minute,
|
||||
values.second,
|
||||
values.millisecond,
|
||||
)
|
||||
.ok_or(DateTimeError::InvalidTime)?;
|
||||
|
||||
let inner = Local
|
||||
.from_local_datetime(&NaiveDateTime::new(date, time))
|
||||
.single()
|
||||
.ok_or(DateTimeError::Ambiguous)?
|
||||
.with_timezone(&Utc);
|
||||
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Formats the `DateTime` using the universal (UTC) time
|
||||
zone, the given format string, and the given locale.
|
||||
|
||||
`format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
|
||||
|
||||
See [`chrono_lc::DateTime::formatl`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn format_string_local(&self, format: Option<&str>, locale: Option<&str>) -> String {
|
||||
self.inner
|
||||
.with_timezone(&Local)
|
||||
.formatl(
|
||||
format.unwrap_or(DEFAULT_FORMAT),
|
||||
locale.unwrap_or(DEFAULT_LOCALE),
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Formats the `DateTime` using the universal (UTC) time
|
||||
zone, the given format string, and the given locale.
|
||||
|
||||
`format` and `locale` default to `"%Y-%m-%d %H:%M:%S"` and `"en"` respectively.
|
||||
|
||||
See [`chrono_lc::DateTime::formatl`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn format_string_universal(&self, format: Option<&str>, locale: Option<&str>) -> String {
|
||||
self.inner
|
||||
.with_timezone(&Utc)
|
||||
.formatl(
|
||||
format.unwrap_or(DEFAULT_FORMAT),
|
||||
locale.unwrap_or(DEFAULT_LOCALE),
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
Parses a time string in the ISO 8601 format, such as
|
||||
`1996-12-19T16:39:57-08:00`, into a new `DateTime` struct.
|
||||
|
||||
See [`chrono::DateTime::parse_from_rfc3339`] for additional details.
|
||||
|
||||
# Errors
|
||||
|
||||
Returns an error if the input string is not a valid RFC 3339 date-time.
|
||||
*/
|
||||
pub fn from_iso_date(iso_date: impl AsRef<str>) -> DateTimeResult<Self> {
|
||||
let inner = ChronoDateTime::parse_from_rfc3339(iso_date.as_ref())?.with_timezone(&Utc);
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
/**
|
||||
Extracts individual date & time values from this
|
||||
`DateTime`, using the current local time zone.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn to_local_time(self) -> DateTimeValues {
|
||||
DateTimeValues::from(self.inner.with_timezone(&Local))
|
||||
}
|
||||
|
||||
/**
|
||||
Extracts individual date & time values from this
|
||||
`DateTime`, using the universal (UTC) time zone.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn to_universal_time(self) -> DateTimeValues {
|
||||
DateTimeValues::from(self.inner.with_timezone(&Utc))
|
||||
}
|
||||
|
||||
/**
|
||||
Formats a time string in the ISO 8601 format, such as `1996-12-19T16:39:57-08:00`.
|
||||
|
||||
See [`chrono::DateTime::to_rfc3339`] for additional details.
|
||||
*/
|
||||
#[must_use]
|
||||
pub fn to_iso_date(self) -> String {
|
||||
self.inner.to_rfc3339()
|
||||
}
|
||||
}
|
||||
|
||||
impl LuaUserData for DateTime {
|
||||
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
|
||||
fields.add_field_method_get("unixTimestamp", |_, this| Ok(this.inner.timestamp()));
|
||||
fields.add_field_method_get("unixTimestampMillis", |_, this| {
|
||||
Ok(this.inner.timestamp_millis())
|
||||
});
|
||||
}
|
||||
|
||||
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
// Metamethods to compare DateTime as instants in time
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Eq,
|
||||
|_, this: &Self, other: LuaUserDataRef<Self>| Ok(this.eq(&other)),
|
||||
);
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Lt,
|
||||
|_, this: &Self, other: LuaUserDataRef<Self>| {
|
||||
Ok(matches!(this.cmp(&other), Ordering::Less))
|
||||
},
|
||||
);
|
||||
methods.add_meta_method(
|
||||
LuaMetaMethod::Le,
|
||||
|_, this: &Self, other: LuaUserDataRef<Self>| {
|
||||
Ok(matches!(this.cmp(&other), Ordering::Less | Ordering::Equal))
|
||||
},
|
||||
);
|
||||
// Normal methods
|
||||
methods.add_method("toIsoDate", |_, this, ()| Ok(this.to_iso_date()));
|
||||
methods.add_method(
|
||||
"formatUniversalTime",
|
||||
|_, this, (format, locale): (Option<String>, Option<String>)| {
|
||||
Ok(this.format_string_universal(format.as_deref(), locale.as_deref()))
|
||||
},
|
||||
);
|
||||
methods.add_method(
|
||||
"formatLocalTime",
|
||||
|_, this, (format, locale): (Option<String>, Option<String>)| {
|
||||
Ok(this.format_string_local(format.as_deref(), locale.as_deref()))
|
||||
},
|
||||
);
|
||||
methods.add_method("toUniversalTime", |_, this: &Self, ()| {
|
||||
Ok(this.to_universal_time())
|
||||
});
|
||||
methods.add_method("toLocalTime", |_, this: &Self, ()| Ok(this.to_local_time()));
|
||||
}
|
||||
}
|
36
crates/lune-std-datetime/src/lib.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
mod date_time;
|
||||
mod result;
|
||||
mod values;
|
||||
|
||||
pub use self::date_time::DateTime;
|
||||
|
||||
/**
|
||||
Creates the `datetime` standard library module.
|
||||
|
||||
# Errors
|
||||
|
||||
Errors when out of memory.
|
||||
*/
|
||||
pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||
TableBuilder::new(lua)?
|
||||
.with_function("fromIsoDate", |_, iso_date: String| {
|
||||
Ok(DateTime::from_iso_date(iso_date)?)
|
||||
})?
|
||||
.with_function("fromLocalTime", |_, values| {
|
||||
Ok(DateTime::from_local_time(&values)?)
|
||||
})?
|
||||
.with_function("fromUniversalTime", |_, values| {
|
||||
Ok(DateTime::from_universal_time(&values)?)
|
||||
})?
|
||||
.with_function("fromUnixTimestamp", |_, timestamp| {
|
||||
Ok(DateTime::from_unix_timestamp_float(timestamp)?)
|
||||
})?
|
||||
.with_function("now", |_, ()| Ok(DateTime::now()))?
|
||||
.build_readonly()
|
||||
}
|
32
crates/lune-std-datetime/src/result.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub type DateTimeResult<T, E = DateTimeError> = Result<T, E>;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum DateTimeError {
|
||||
#[error("invalid date")]
|
||||
InvalidDate,
|
||||
#[error("invalid time")]
|
||||
InvalidTime,
|
||||
#[error("ambiguous date or time")]
|
||||
Ambiguous,
|
||||
#[error("date or time is outside allowed range")]
|
||||
OutOfRangeUnspecified,
|
||||
#[error("{name} must be within range {min} -> {max}, got {value}")]
|
||||
OutOfRange {
|
||||
name: &'static str,
|
||||
value: String,
|
||||
min: String,
|
||||
max: String,
|
||||
},
|
||||
#[error(transparent)]
|
||||
ParseError(#[from] chrono::ParseError),
|
||||
}
|
||||
|
||||
impl From<DateTimeError> for LuaError {
|
||||
fn from(value: DateTimeError) -> Self {
|
||||
LuaError::runtime(value.to_string())
|
||||
}
|
||||
}
|
170
crates/lune-std-datetime/src/values.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
use mlua::prelude::*;
|
||||
|
||||
use chrono::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
|
||||
use super::result::{DateTimeError, DateTimeResult};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct DateTimeValues {
|
||||
pub year: i32,
|
||||
pub month: u32,
|
||||
pub day: u32,
|
||||
pub hour: u32,
|
||||
pub minute: u32,
|
||||
pub second: u32,
|
||||
pub millisecond: u32,
|
||||
}
|
||||
|
||||
impl DateTimeValues {
|
||||
/**
|
||||
Verifies that all of the date & time values are within allowed ranges:
|
||||
|
||||
| Name | Range |
|
||||
|---------------|----------------|
|
||||
| `year` | `1400 -> 9999` |
|
||||
| `month` | `1 -> 12` |
|
||||
| `day` | `1 -> 31` |
|
||||
| `hour` | `0 -> 23` |
|
||||
| `minute` | `0 -> 59` |
|
||||
| `second` | `0 -> 60` |
|
||||
| `millisecond` | `0 -> 999` |
|
||||
*/
|
||||
pub fn verify(self) -> DateTimeResult<Self> {
|
||||
verify_in_range("year", self.year, 1400, 9999)?;
|
||||
verify_in_range("month", self.month, 1, 12)?;
|
||||
verify_in_range("day", self.day, 1, 31)?;
|
||||
verify_in_range("hour", self.hour, 0, 23)?;
|
||||
verify_in_range("minute", self.minute, 0, 59)?;
|
||||
verify_in_range("second", self.second, 0, 60)?;
|
||||
verify_in_range("millisecond", self.millisecond, 0, 999)?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_in_range<T>(name: &'static str, value: T, min: T, max: T) -> DateTimeResult<T>
|
||||
where
|
||||
T: PartialOrd + std::fmt::Display,
|
||||
{
|
||||
assert!(max > min);
|
||||
if value < min || value > max {
|
||||
Err(DateTimeError::OutOfRange {
|
||||
name,
|
||||
min: min.to_string(),
|
||||
max: max.to_string(),
|
||||
value: value.to_string(),
|
||||
})
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Conversion methods between `DateTimeValues` and plain lua tables
|
||||
|
||||
Note that the `IntoLua` implementation here uses a read-only table,
|
||||
since we generally want to convert into lua when we know we have
|
||||
a fixed point in time, and we guarantee that it doesn't change
|
||||
*/
|
||||
|
||||
impl FromLua<'_> for DateTimeValues {
|
||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||
if !value.is_table() {
|
||||
return Err(LuaError::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "DateTimeValues",
|
||||
message: Some("value must be a table".to_string()),
|
||||
});
|
||||
};
|
||||
|
||||
let value = value.as_table().unwrap();
|
||||
let values = Self {
|
||||
year: value.get("year")?,
|
||||
month: value.get("month")?,
|
||||
day: value.get("day")?,
|
||||
hour: value.get("hour")?,
|
||||
minute: value.get("minute")?,
|
||||
second: value.get("second")?,
|
||||
millisecond: value.get("millisecond").unwrap_or(0),
|
||||
};
|
||||
|
||||
match values.verify() {
|
||||
Ok(dt) => Ok(dt),
|
||||
Err(e) => Err(LuaError::FromLuaConversionError {
|
||||
from: "table",
|
||||
to: "DateTimeValues",
|
||||
message: Some(e.to_string()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoLua<'_> for DateTimeValues {
|
||||
fn into_lua(self, lua: &Lua) -> LuaResult<LuaValue> {
|
||||
let tab = TableBuilder::new(lua)?
|
||||
.with_value("year", self.year)?
|
||||
.with_values(vec![
|
||||
("month", self.month),
|
||||
("day", self.day),
|
||||
("hour", self.hour),
|
||||
("minute", self.minute),
|
||||
("second", self.second),
|
||||
("millisecond", self.millisecond),
|
||||
])?
|
||||
.build_readonly()?;
|
||||
Ok(LuaValue::Table(tab))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Conversion methods between chrono's timezone-aware `DateTime` to
|
||||
and from our non-timezone-aware `DateTimeValues` values struct
|
||||
*/
|
||||
|
||||
impl<T: TimeZone> From<DateTime<T>> for DateTimeValues {
|
||||
fn from(value: DateTime<T>) -> Self {
|
||||
Self {
|
||||
year: value.year(),
|
||||
month: value.month(),
|
||||
day: value.day(),
|
||||
hour: value.hour(),
|
||||
minute: value.minute(),
|
||||
second: value.second(),
|
||||
millisecond: value.timestamp_subsec_millis(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DateTimeValues> for DateTime<Utc> {
|
||||
type Error = DateTimeError;
|
||||
fn try_from(value: DateTimeValues) -> Result<Self, Self::Error> {
|
||||
Utc.with_ymd_and_hms(
|
||||
value.year,
|
||||
value.month,
|
||||
value.day,
|
||||
value.hour,
|
||||
value.minute,
|
||||
value.second,
|
||||
)
|
||||
.single()
|
||||
.ok_or(DateTimeError::Ambiguous)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DateTimeValues> for DateTime<Local> {
|
||||
type Error = DateTimeError;
|
||||
fn try_from(value: DateTimeValues) -> Result<Self, Self::Error> {
|
||||
Local
|
||||
.with_ymd_and_hms(
|
||||
value.year,
|
||||
value.month,
|
||||
value.day,
|
||||
value.hour,
|
||||
value.minute,
|
||||
value.second,
|
||||
)
|
||||
.single()
|
||||
.ok_or(DateTimeError::Ambiguous)
|
||||
}
|
||||
}
|
23
crates/lune-std-fs/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "lune-std-fs"
|
||||
version = "0.1.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/lune-org/lune"
|
||||
description = "Lune standard library - FS"
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
mlua = { version = "0.9.9", features = ["luau"] }
|
||||
|
||||
bstr = "1.9"
|
||||
|
||||
tokio = { version = "1", default-features = false, features = ["fs"] }
|
||||
|
||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
||||
lune-std-datetime = { version = "0.1.2", path = "../lune-std-datetime" }
|
166
crates/lune-std-fs/src/copy.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use mlua::prelude::*;
|
||||
use tokio::fs;
|
||||
|
||||
use super::options::FsWriteOptions;
|
||||
|
||||
pub struct CopyContents {
|
||||
// Vec<(relative depth, path)>
|
||||
pub dirs: Vec<(usize, PathBuf)>,
|
||||
pub files: Vec<(usize, PathBuf)>,
|
||||
}
|
||||
|
||||
async fn get_contents_at(root: PathBuf, _: FsWriteOptions) -> LuaResult<CopyContents> {
|
||||
let mut dirs = Vec::new();
|
||||
let mut files = Vec::new();
|
||||
|
||||
let mut queue = VecDeque::new();
|
||||
|
||||
let normalized_root = fs::canonicalize(&root).await.map_err(|e| {
|
||||
LuaError::RuntimeError(format!("Failed to canonicalize root directory path\n{e}"))
|
||||
})?;
|
||||
|
||||
// Push initial children of the root path into the queue
|
||||
let mut entries = fs::read_dir(&normalized_root).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
queue.push_back((1, entry.path()));
|
||||
}
|
||||
|
||||
// Go through the current queue, pushing to it
|
||||
// when we find any new descendant directories
|
||||
// FUTURE: Try to do async reading here concurrently to speed it up a bit
|
||||
while let Some((current_depth, current_path)) = queue.pop_front() {
|
||||
let meta = fs::metadata(¤t_path).await?;
|
||||
if meta.is_symlink() {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"Symlinks are not yet supported, encountered at path '{}'",
|
||||
current_path.display()
|
||||
)));
|
||||
} else if meta.is_dir() {
|
||||
// FUTURE: Add an option in FsWriteOptions for max depth and limit it here
|
||||
let mut entries = fs::read_dir(¤t_path).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
queue.push_back((current_depth + 1, entry.path()));
|
||||
}
|
||||
dirs.push((current_depth, current_path));
|
||||
} else {
|
||||
files.push((current_depth, current_path));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that all directory and file paths are relative to the root path
|
||||
// SAFETY: Since we only ever push dirs and files relative to the root, unwrap is safe
|
||||
for (_, dir) in &mut dirs {
|
||||
*dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf();
|
||||
}
|
||||
for (_, file) in &mut files {
|
||||
*file = file.strip_prefix(&normalized_root).unwrap().to_path_buf();
|
||||
}
|
||||
|
||||
// FUTURE: Deduplicate paths such that these directories:
|
||||
// - foo/
|
||||
// - foo/bar/
|
||||
// - foo/bar/baz/
|
||||
// turn into a single foo/bar/baz/ and let create_dir_all do the heavy lifting
|
||||
|
||||
Ok(CopyContents { dirs, files })
|
||||
}
|
||||
|
||||
async fn ensure_no_dir_exists(path: impl AsRef<Path>) -> LuaResult<()> {
|
||||
let path = path.as_ref();
|
||||
match fs::metadata(&path).await {
|
||||
Ok(meta) if meta.is_dir() => Err(LuaError::RuntimeError(format!(
|
||||
"A directory already exists at the path '{}'",
|
||||
path.display()
|
||||
))),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_no_file_exists(path: impl AsRef<Path>) -> LuaResult<()> {
|
||||
let path = path.as_ref();
|
||||
match fs::metadata(&path).await {
|
||||
Ok(meta) if meta.is_file() => Err(LuaError::RuntimeError(format!(
|
||||
"A file already exists at the path '{}'",
|
||||
path.display()
|
||||
))),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn copy(
|
||||
source: impl AsRef<Path>,
|
||||
target: impl AsRef<Path>,
|
||||
options: FsWriteOptions,
|
||||
) -> LuaResult<()> {
|
||||
let source = source.as_ref();
|
||||
let target = target.as_ref();
|
||||
|
||||
// Check if we got a file or directory - we will handle them differently below
|
||||
let (is_dir, is_file) = match fs::metadata(&source).await {
|
||||
Ok(meta) => (meta.is_dir(), meta.is_file()),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"No file or directory exists at the path '{}'",
|
||||
source.display()
|
||||
)))
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
if !is_file && !is_dir {
|
||||
return Err(LuaError::RuntimeError(format!(
|
||||
"The given path '{}' is not a file or a directory",
|
||||
source.display()
|
||||
)));
|
||||
}
|
||||
|
||||
// Perform copying:
|
||||
//
|
||||
// 1. If we are not allowed to overwrite, make sure nothing exists at the target path
|
||||
// 2. If we are allowed to overwrite, remove any previous entry at the path
|
||||
// 3. Write all directories first
|
||||
// 4. Write all files
|
||||
|
||||
if !options.overwrite {
|
||||
if is_file {
|
||||
ensure_no_file_exists(target).await?;
|
||||
} else if is_dir {
|
||||
ensure_no_dir_exists(target).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if is_file {
|
||||
fs::copy(source, target).await?;
|
||||
} else if is_dir {
|
||||
let contents = get_contents_at(source.to_path_buf(), options).await?;
|
||||
|
||||
if options.overwrite {
|
||||
let (is_dir, is_file) = match fs::metadata(&target).await {
|
||||
Ok(meta) => (meta.is_dir(), meta.is_file()),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => (false, false),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
if is_dir {
|
||||
fs::remove_dir_all(target).await?;
|
||||
} else if is_file {
|
||||
fs::remove_file(target).await?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(target).await?;
|
||||
|
||||
// FUTURE: Write dirs / files concurrently
|
||||
// to potentially speed these operations up
|
||||
for (_, dir) in &contents.dirs {
|
||||
fs::create_dir_all(target.join(dir)).await?;
|
||||
}
|
||||
for (_, file) in &contents.files {
|
||||
fs::copy(source.join(file), target.join(file)).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|