Compare commits
1455 Commits
debian/v1.
...
master
Author | SHA1 | Date | |
---|---|---|---|
e66ff65eaf | |||
b0363a4414 | |||
2bea8813af | |||
4fbbee06f4 | |||
dce2c86fc2 | |||
0582a26af9 | |||
d7df8f6978 | |||
f75ed771e7 | |||
28805c62f4 | |||
ae81170396 | |||
|
4875a8fed9 | ||
|
b098b5f37d | ||
|
7b52c13fae | ||
|
0a6d9e88bc | ||
|
8205775dc9 | ||
|
d81ecb3199 | ||
|
986f826830 | ||
|
06483fd586 | ||
|
21e400a4a1 | ||
|
591467f1e3 | ||
|
812485f863 | ||
|
233c3d1abe | ||
|
eac4bb2875 | ||
|
5e11cccdf9 | ||
|
923a134270 | ||
|
a775a01d2a | ||
|
3d29fcad7e | ||
|
ecb72d0fd1 | ||
|
5bdb5d98a9 | ||
|
a59c07d8a3 | ||
|
060c844468 | ||
|
07c01d81bf | ||
|
69a26a0e55 | ||
|
445599416c | ||
|
e8a78a0646 | ||
|
94959e2e91 | ||
|
257162c451 | ||
|
cf6d461247 | ||
|
d1122d75ec | ||
|
d8c10ce10a | ||
|
065404758b | ||
|
6e6cdc7bde | ||
|
6c5f330b71 | ||
|
b02c09fc7b | ||
|
be3c1eea6f | ||
|
0fe2de090e | ||
|
5c3036e18b | ||
|
a1107bd20e | ||
|
07ba532c5e | ||
|
9b29fde1b3 | ||
|
dc58f16418 | ||
|
2cbf5b56af | ||
|
d3bd99b5ee | ||
|
7c6105d93a | ||
|
24640d13bb | ||
|
ced08e69c9 | ||
|
bb435f3580 | ||
|
64e86411c5 | ||
|
2f632ae472 | ||
|
f3e498439c | ||
|
dfad0a7b57 | ||
|
6a7385947d | ||
|
7c2bf8700b | ||
|
780d92782c | ||
|
168156b7ff | ||
|
4ca763c028 | ||
|
550ffaf8a8 | ||
|
8eb346de25 | ||
|
7ef6431b64 | ||
|
806b97afab | ||
|
29c0b57f0c | ||
|
02ed853399 | ||
|
c32a523923 | ||
|
963486b663 | ||
|
a9ecbeec84 | ||
|
24249a1e74 | ||
|
609af979ff | ||
|
678fb71a7a | ||
|
303251b465 | ||
|
0932e1de73 | ||
|
ab337409a1 | ||
|
3aec774cf3 | ||
|
a82f4e1813 | ||
|
d8ecae64f0 | ||
|
82a1a5fc1e | ||
|
ecc6321e17 | ||
|
3487fa90e0 | ||
|
44c99fb0ef | ||
|
80c314bb9e | ||
|
26fa566d85 | ||
|
75ba9ddae8 | ||
|
f6b8677c2e | ||
|
e5979998c5 | ||
|
7d73ca321d | ||
|
0614fa7b47 | ||
|
e54732be1b | ||
|
6fafb727ba | ||
|
ba89d9b122 | ||
|
8768374460 | ||
|
832c4ab300 | ||
|
ec1cc92c8b | ||
|
c1dc438138 | ||
|
78698ff17f | ||
|
9bdda210f5 | ||
|
d3964ae25b | ||
|
20164417fb | ||
|
4e042389c5 | ||
|
578fa21591 | ||
|
8b6c16fd30 | ||
|
210a789aa4 | ||
|
a4b8a8422f | ||
|
bbc2ac2550 | ||
|
61b8d584af | ||
|
16c08d86f2 | ||
|
7d0b573d37 | ||
|
15cdc54ec3 | ||
|
f905426ebd | ||
|
24a1f324c7 | ||
|
e073d70098 | ||
|
ca74f92051 | ||
|
c21ea8803a | ||
|
213af0cfa3 | ||
|
fa233e13c6 | ||
|
c1788424b5 | ||
|
461dee776c | ||
|
49f68451ae | ||
|
d3f120f2bb | ||
|
1db22d82af | ||
|
2513f8b3d6 | ||
|
eac5e8adc8 | ||
|
ee4ec7ba97 | ||
|
0f6ecfc311 | ||
|
624593e425 | ||
|
1c48feeb61 | ||
|
7406b62093 | ||
|
89baafba2f | ||
|
4c028bfcb3 | ||
|
dd89f12c2e | ||
|
6a195be814 | ||
|
c573e9fbaf | ||
|
6c6a7e6fe4 | ||
|
b9bf27983f | ||
|
7da975749c | ||
|
854d8775d5 | ||
|
f20c39f6a4 | ||
|
603045bcad | ||
|
5a3d510f05 | ||
|
ec3120cf8f | ||
|
dbd6ce5bdc | ||
|
d4470568ec | ||
|
145117dc58 | ||
|
e1d454d834 | ||
|
db82ae4b88 | ||
|
f4df12e5ff | ||
|
934e24f837 | ||
|
0b08d9251e | ||
|
60a82c5eb2 | ||
|
e216b6caf4 | ||
|
6c78717d97 | ||
|
68c626088d | ||
|
b03e7ec8f6 | ||
|
4d3bce0266 | ||
|
6c0d8942e5 | ||
|
7f677cf5b9 | ||
|
1f3b712097 | ||
|
f8ae189cee | ||
|
c886feca79 | ||
|
385ea1360e | ||
|
820767db3d | ||
|
65c472d936 | ||
|
d809682c08 | ||
|
ab1a9b358a | ||
|
4538f6d915 | ||
|
fb7fe0dc0a | ||
|
fbff7491df | ||
|
f84c4b3276 | ||
|
251d14a470 | ||
|
5eb6c05189 | ||
|
b81b1ff68b | ||
|
c29c4bfaa4 | ||
|
da8cc661b7 | ||
|
1ee0b28f66 | ||
|
1efcbb4529 | ||
|
78e0d1bead | ||
|
6832fe4990 | ||
|
14a31d81ea | ||
|
ee432341da | ||
|
b6d571500e | ||
|
4a40c08578 | ||
|
4abf7208ff | ||
|
f8b506144d | ||
|
bbd6613558 | ||
|
b201165b81 | ||
|
c13839ce8d | ||
|
958fc9eee8 | ||
|
70d5c7f658 | ||
|
79e83d3782 | ||
|
a8d6024352 | ||
|
8b76e5bc36 | ||
|
79a5916057 | ||
|
a7699edd6d | ||
|
497a7c3467 | ||
|
cdf4023928 | ||
|
b7692dd355 | ||
|
f87422187f | ||
|
ee906dc52b | ||
|
524a6c2c4d | ||
|
726f52976d | ||
|
d0c0f3e7fc | ||
|
57bfc1001f | ||
|
3c9332f9c1 | ||
|
72583a58d9 | ||
|
59fa68e063 | ||
|
5255847452 | ||
|
60eefbce94 | ||
|
9dd3e8ff85 | ||
|
900c53f499 | ||
|
2f2aa55fd7 | ||
|
358d9a79d3 | ||
|
908b78fdda | ||
|
b33e271d2e | ||
|
2db446889b | ||
|
a265290e41 | ||
|
02108ce977 | ||
|
7bb0175648 | ||
|
f6fcb37d91 | ||
|
8c3b708981 | ||
|
a7424b9329 | ||
|
2e34420605 | ||
|
1e69bb0f49 | ||
|
2b5211d098 | ||
|
7d758af900 | ||
|
ad5489c102 | ||
|
b8996d5abb | ||
|
8d1943d6e9 | ||
|
ce26294250 | ||
|
4650f4e8cf | ||
|
b44c219441 | ||
|
bf0c333266 | ||
|
ff5c2c5e58 | ||
|
d2bf7b8fde | ||
|
e8ba9e9785 | ||
|
3f1c5246f8 | ||
|
ccea46fe2b | ||
|
d033b78c58 | ||
|
8b099e5725 | ||
|
3f954d273c | ||
|
e77d4dda84 | ||
|
2fbf8f0a10 | ||
|
781c69387c | ||
|
8ecef9eadc | ||
|
16d45c921f | ||
|
62c163f11e | ||
|
9b13a6780c | ||
|
f247ec44f1 | ||
|
8c96b514d4 | ||
|
63c4eeb4c7 | ||
|
7922de10ff | ||
|
553902e52a | ||
|
9f39cf154a | ||
|
8d0f53ada0 | ||
|
bc45c93932 | ||
|
7ef27a0040 | ||
|
4728c3a59a | ||
|
950ffaafda | ||
|
483191c521 | ||
|
2e111aa13a | ||
|
a3eebb05ba | ||
|
74b5e2797f | ||
|
801ec3bf4c | ||
|
8a6419a3b7 | ||
|
bb8583872b | ||
|
0c374aba48 | ||
|
d26325d8dc | ||
|
5a855fb7e4 | ||
|
680e538fd6 | ||
|
9006283c90 | ||
|
6ed4857b1d | ||
|
8c780f07c6 | ||
|
f1bd92fa52 | ||
|
ff3e844b68 | ||
|
34d81b0e6c | ||
|
91bc524e06 | ||
|
beed50874b | ||
|
01bd36ca3f | ||
|
7cf9f8ff10 | ||
|
f92b728a8e | ||
|
fae57e36e3 | ||
|
8b4764912a | ||
|
e3896bf5e7 | ||
|
3be328af35 | ||
|
fbddd54059 | ||
|
f0b83663f6 | ||
|
bfd418d8b5 | ||
|
62227bb15f | ||
|
aa462258ff | ||
|
59c22c8dfc | ||
|
e5b3088b5e | ||
|
3080e1fdc7 | ||
|
94517c0f4b | ||
|
05646718a9 | ||
|
e553392d9d | ||
|
352b05b1ea | ||
|
7cabf64b11 | ||
|
d071f5fa90 | ||
|
b50253a529 | ||
|
d23820227e | ||
|
7471da6c26 | ||
|
9f310383ba | ||
|
5626e45f4a | ||
|
e715ca42c4 | ||
|
1e6433a65e | ||
|
c07d853588 | ||
|
24cd17ac51 | ||
|
7432f15e27 | ||
|
0530735547 | ||
|
8dd404801a | ||
|
30b22d87a6 | ||
|
36df585a5e | ||
|
1db11a45e3 | ||
|
2a0d3759b1 | ||
|
3a3e5d5f3b | ||
|
2ea7b0cff1 | ||
|
65b9da926b | ||
|
d7a5435d9e | ||
|
6cf86ec8dc | ||
|
149b417d8d | ||
|
699097c55b | ||
|
9ccd967cb8 | ||
|
0a44e3b4a1 | ||
|
fa9929757d | ||
|
f60678478b | ||
|
09125621d8 | ||
|
b29f649822 | ||
|
62682cb18c | ||
|
76362e81e8 | ||
|
317fe28286 | ||
|
4160235b92 | ||
|
7743bda3eb | ||
|
bf1953fe22 | ||
|
f2ca021740 | ||
|
4f0bb95a38 | ||
|
d2b8d7dc04 | ||
|
a0d863569b | ||
|
ce16dab65f | ||
|
ba5fa06306 | ||
|
6cb0f14e5a | ||
|
86858a176e | ||
|
68a4772627 | ||
|
8d2da6bdc7 | ||
|
49b1a55042 | ||
|
cc048f9b20 | ||
|
e584c16aaa | ||
|
606e9093e1 | ||
|
181232c1e6 | ||
|
1d5ad96646 | ||
|
949da0529f | ||
|
295d413fdc | ||
|
04fe8fdd1e | ||
|
8f31380084 | ||
|
93238abd76 | ||
|
3191930fdb | ||
|
095a486621 | ||
|
52fe79df47 | ||
|
4fac9eb928 | ||
|
e2b8d3b6ef | ||
|
1b5042ef6a | ||
|
3f143ba912 | ||
|
80601ae432 | ||
|
49c2ba3c4b | ||
|
0272e4f691 | ||
|
7971daa27f | ||
|
ad60993b9a | ||
|
7244fcd128 | ||
|
c0547637b5 | ||
|
5315628a10 | ||
|
df0546a2f3 | ||
|
4f8273bae7 | ||
|
b4501a4d2a | ||
|
96ff1f3af1 | ||
|
bc9131119f | ||
|
19baf7f582 | ||
|
d7b552a513 | ||
|
1e20466f74 | ||
|
a0d73f27d9 | ||
|
e6ae343975 | ||
|
da03e60de4 | ||
|
ae3e2a09d9 | ||
|
d2816220eb | ||
|
7576fbbe6d | ||
|
12108bc529 | ||
|
10c08dea6d | ||
|
4536a63e69 | ||
|
ebcf4a6ae3 | ||
|
a0d3685227 | ||
|
cb7b98de0d | ||
|
706f531ca2 | ||
|
12a150c817 | ||
|
a195870cfb | ||
|
833c15b2d5 | ||
|
cf7bfe79ca | ||
|
7c956d1481 | ||
|
5cd3ab4b4c | ||
|
f3c4d344b3 | ||
|
29c1ea25c7 | ||
|
32290de8dd | ||
|
428fbaae02 | ||
|
54fab3d233 | ||
|
49eb28dacf | ||
|
60b6e7baa1 | ||
|
8c43c2e6e8 | ||
|
ad95fa5e11 | ||
|
41c31d3d2f | ||
|
bf6ce94b80 | ||
|
2f76a24dff | ||
|
7fdbcde71c | ||
|
99a9b5f9a2 | ||
|
c4a9da41ba | ||
|
8c4ead8421 | ||
|
da00fa7868 | ||
|
1f7e964a7d | ||
|
7e4292f0fd | ||
|
9a150cbcca | ||
|
821e224aac | ||
|
edfe440c6e | ||
|
c2b3d23ec0 | ||
|
4d4c9c8c6e | ||
|
e32f433688 | ||
|
86c32cf9ff | ||
|
1ffd198b00 | ||
|
0c796dff90 | ||
|
2ebf05f7d6 | ||
|
5fa8c1de9a | ||
|
d6bd883c94 | ||
|
8a64491952 | ||
|
0d29ef5787 | ||
|
5a8e7d277f | ||
|
ac825bf725 | ||
|
4c3097155b | ||
|
f3cfe7ec82 | ||
|
9e5cc3b9a7 | ||
|
096baa8897 | ||
|
0bfac1a164 | ||
|
06be65c12f | ||
|
1d45bab46d | ||
|
82b95faa53 | ||
|
d0a85b0870 | ||
|
aa9e5c4543 | ||
|
1845b8f66e | ||
|
151e63ff91 | ||
|
736ac19ed0 | ||
|
d2413896c7 | ||
|
482a6da1d8 | ||
|
005a3c5a0d | ||
|
8d57886bae | ||
|
02a21209ec | ||
|
aa38d1a5bd | ||
|
0d27b4ad43 | ||
|
3702173cd4 | ||
|
e8104f63ac | ||
|
5b979fa151 | ||
|
f7daf16ac5 | ||
|
f4ce5f9a3b | ||
|
46a2e3f453 | ||
|
e86892b9a5 | ||
|
8105e966ff | ||
|
45a56c2e02 | ||
|
434b112c65 | ||
|
bced495570 | ||
|
c7fdfed00c | ||
|
22a46a98ea | ||
|
fc1a7afd9a | ||
|
1db2e2aba9 | ||
|
dbe7dc4ce0 | ||
|
9c1926f636 | ||
|
d10e2dbca7 | ||
|
a5b44be6a1 | ||
|
ddecf48697 | ||
|
56fa2e49f6 | ||
|
bc24a6b1db | ||
|
06290cdd78 | ||
|
154f8f03da | ||
|
fbcb9d898d | ||
|
d051650457 | ||
|
af613f5b5b | ||
|
e0fb31fc58 | ||
|
ac282db1ac | ||
|
56cf535551 | ||
|
6b9b999996 | ||
|
5bb29e99ba | ||
|
ce3c53ae3f | ||
|
a09f27b448 | ||
|
570110f831 | ||
|
bc1c6a4986 | ||
|
87e4367455 | ||
|
2cd5b7f0a8 | ||
|
92fee33b64 | ||
|
d1e5d57459 | ||
|
2e3489a489 | ||
|
d644b43e24 | ||
|
7c11441f50 | ||
|
190007a21f | ||
|
238d5c22db | ||
|
f9992876df | ||
|
588af2e3cc | ||
|
383bbbdafe | ||
|
3df5eece38 | ||
|
707040b506 | ||
|
82832b7055 | ||
|
4713010929 | ||
|
1ad9f5b045 | ||
|
d781b2eb91 | ||
|
fe585297fc | ||
|
2d8098b06d | ||
|
1436b15328 | ||
|
4293ea320d | ||
|
d568fd596b | ||
|
952e58ecc1 | ||
|
71e3b9dfc1 | ||
|
14b4229019 | ||
|
35f0c945fd | ||
|
2480d13f58 | ||
|
e07f4bf96f | ||
|
fbe7440e24 | ||
|
f2d382681b | ||
|
e60c69b394 | ||
|
7bd444ade9 | ||
|
c177f0a1b3 | ||
|
41748a4036 | ||
|
4991a70bba | ||
|
3b1d4d630c | ||
|
02e5860e43 | ||
|
f52b459872 | ||
|
3ab069d650 | ||
|
9d4ffe542c | ||
|
2b7481383f | ||
|
9c803a69ff | ||
|
74975ac9d8 | ||
|
ebdcf8804d | ||
|
bd96a32d5b | ||
|
41ac51f291 | ||
|
6ac25d344b | ||
|
315fe7ca82 | ||
|
7ffe564e8e | ||
|
6c616f73eb | ||
|
2a1af2a131 | ||
|
880b2d4b71 | ||
|
eb8404ac91 | ||
|
5f7361156b | ||
|
897a01bb79 | ||
|
73d589ad11 | ||
|
4b4356e7ff | ||
|
3d657b4a18 | ||
|
64065b2798 | ||
|
95af568653 | ||
|
fc9865e273 | ||
|
cdd61bdb2c | ||
|
9e8bed7aab | ||
|
3d5c222957 | ||
|
a146065cc6 | ||
|
ba6e6235d6 | ||
|
5a56c20221 | ||
|
c1cd464a7b | ||
|
88f2bfdf39 | ||
|
72a9f901ab | ||
|
2aba6b1c27 | ||
|
807abaa2c0 | ||
|
a0aeb3b1f8 | ||
|
9fa7c7aac0 | ||
|
a01bd73e93 | ||
|
84df25f863 | ||
|
756a495ac6 | ||
|
68ff1b9e17 | ||
|
9e16bfc366 | ||
|
9b2914cc63 | ||
|
31811ab906 | ||
|
ae5617f3a0 | ||
|
6d4c2cfd39 | ||
|
c0b09db05e | ||
|
fb2bd1c346 | ||
|
285f1ebe89 | ||
|
ddabda1c4b | ||
|
1435ec89c9 | ||
|
1bb6d893e0 | ||
|
3cfb74be54 | ||
|
8790a9c680 | ||
|
080d495044 | ||
|
181545e6b7 | ||
|
aa19e8a7cd | ||
|
cd49fd5b83 | ||
|
f435cecfdc | ||
|
7285122fed | ||
|
9d518eded8 | ||
|
d43202d90d | ||
|
f43deff626 | ||
|
4f513fb1e0 | ||
|
78524e64bd | ||
|
2d55f982d3 | ||
|
b336e3b08a | ||
|
83bd19c8dc | ||
|
6e65952d75 | ||
|
936f5d0301 | ||
|
fe59b4512b | ||
|
bfd9ae324b | ||
|
ac3d1240ae | ||
|
4dcd8cfb18 | ||
|
697f316780 | ||
|
9432c82386 | ||
|
d955ddfe82 | ||
|
18e4d2ba4e | ||
|
dbd5b80636 | ||
|
0d3c697add | ||
|
71bf6b362b | ||
|
214226a271 | ||
|
d8176c2f8d | ||
|
f840034d9c | ||
|
3d76f76d3e | ||
|
5c7ea7b258 | ||
|
66e340dece | ||
|
a4a123c331 | ||
|
510477b99b | ||
|
84948eb9da | ||
|
c30c6b7fbd | ||
|
c5587600ba | ||
|
d0d191fc28 | ||
|
f217fc8aad | ||
|
c9fa9b9a92 | ||
|
f2ca7b2b16 | ||
|
285079cae2 | ||
|
d147661944 | ||
|
18a65651f3 | ||
|
e43ea3aa5e | ||
|
5f43aeba85 | ||
|
091f3aa7ee | ||
|
e96cb6930b | ||
|
3983d0c5bc | ||
|
b2ea368097 | ||
|
1ce801cced | ||
|
50f3af81e4 | ||
|
fecbc8a018 | ||
|
41a1f56bd5 | ||
|
01bfbc36b5 | ||
|
f7127bfb5a | ||
|
a093298993 | ||
|
a583a9dc2a | ||
|
28effd8ad4 | ||
|
e2e94c11ff | ||
|
022d1fec63 | ||
|
b9a2433283 | ||
|
52fd9067b8 | ||
|
b1cb12861f | ||
|
3cfbcc6b02 | ||
|
e65e8b2097 | ||
|
ae9af56661 | ||
|
cb5bdb5fea | ||
|
e6f609d3be | ||
|
64c569c42f | ||
|
41c60579a6 | ||
|
e50292a2ba | ||
|
0a1c01fb26 | ||
|
17525e1e15 | ||
|
6f6ad949ca | ||
|
24811e0a31 | ||
|
bef5e139c0 | ||
|
d51ac45079 | ||
|
5116b82e77 | ||
|
e5f7c62e25 | ||
|
34cb0ebf39 | ||
|
19bd1045e1 | ||
|
28e4f59e50 | ||
|
46e6917df6 | ||
|
a1f1927b6d | ||
|
cb78ff333b | ||
|
86fa869b20 | ||
|
0aa93543bc | ||
|
bf17ad4567 | ||
|
c30de8c6db | ||
|
4ea6a12d20 | ||
|
194d6a6414 | ||
|
abd53b6251 | ||
|
62a4814961 | ||
|
9bcdc9ba33 | ||
|
310e131a6e | ||
|
369d453455 | ||
|
ea1460abaf | ||
|
1806ef9d7e | ||
|
a7ced533c0 | ||
|
280c59e6b5 | ||
|
5b4a72ea1f | ||
|
d29c0cc99f | ||
|
8644389d7e | ||
|
c5b5326480 | ||
|
e1611969ce | ||
|
65cefb3584 | ||
|
bba26fdc20 | ||
|
63391e657f | ||
|
01d02124d1 | ||
|
a7d2c600ff | ||
|
50fcc1caaa | ||
|
164f88ef4c | ||
|
bfcdd31ed2 | ||
|
cb2485bab0 | ||
|
8f3d9277ac | ||
|
33ee190b0f | ||
|
3c77ce945d | ||
|
c5949a0337 | ||
|
ef0d422a49 | ||
|
285712bfc7 | ||
|
1cd18db560 | ||
|
95bd639124 | ||
|
3afc725af6 | ||
|
6462773e7f | ||
|
b457ce493b | ||
|
dad8cdef8b | ||
|
65d1e4e768 | ||
|
ec75509725 | ||
|
445449d2db | ||
|
81f2c8b746 | ||
|
c264364752 | ||
|
723001e1e7 | ||
|
389ca25b6a | ||
|
4cf5a6f7a0 | ||
|
5c3a207c72 | ||
|
06920a2271 | ||
|
9ab73c0aa0 | ||
|
feab5da2ad | ||
|
49d627ca38 | ||
|
3fd4f9d384 | ||
|
1e57028e6e | ||
|
319db95bc8 | ||
|
0c5774a48f | ||
|
71a5e29ae4 | ||
|
86cb118378 | ||
|
46ae4fda74 | ||
|
303d2a7837 | ||
|
7f79c2dc4a | ||
|
d38cec0b51 | ||
|
651b0a4518 | ||
|
ed84dacb9c | ||
|
330a795fee | ||
|
917f3728d5 | ||
|
9643c7adf9 | ||
|
f18291e9d2 | ||
|
0b161a2368 | ||
|
b182a08ca2 | ||
|
b0eb241fc3 | ||
|
bbdf0a1289 | ||
|
7f61e9addd | ||
|
abfa010bbf | ||
|
769e0fcc29 | ||
|
c7fa5cab8b | ||
|
f536d538ea | ||
|
7d12d8561a | ||
|
4c8ee41cf2 | ||
|
63d4c99d15 | ||
|
ac97cf5772 | ||
|
d28f0bed50 | ||
|
8c8649b584 | ||
|
66bceb577a | ||
|
120f19af65 | ||
|
116c56be81 | ||
|
a094e16c1f | ||
|
dcc7f2a686 | ||
|
c79603d7fa | ||
|
2f282e3469 | ||
|
4903911d62 | ||
|
a666bf310b | ||
|
6ed8ce9af1 | ||
|
e4708149e0 | ||
|
8d59680de5 | ||
|
5ed61012f0 | ||
|
52a90e52d4 | ||
|
3e464580ea | ||
|
10c3d6dee2 | ||
|
a0418ddb78 | ||
|
d1178b1a01 | ||
|
5472d220ba | ||
|
06b7f5ba2f | ||
|
3cf0a9ee84 | ||
|
0828dd8af3 | ||
|
3f0ef2d31a | ||
|
f6d3783f6c | ||
|
3eab074e2a | ||
|
ecbadda65a | ||
|
9f9cf5a3bb | ||
|
4cac260f81 | ||
|
f4209515a1 | ||
|
d2be9fa931 | ||
|
c0bfec7b11 | ||
|
a5f0cadccd | ||
|
6237672855 | ||
|
54d392be82 | ||
|
95d8cf9257 | ||
|
12da5e64f8 | ||
|
a3eb715414 | ||
|
64847cd465 | ||
|
b20b4abb9e | ||
|
24cc340cb9 | ||
|
2a2cea2cd5 | ||
|
bdb32a29e1 | ||
|
633fa9f870 | ||
|
af67d9b513 | ||
|
b7934d9d03 | ||
|
bdc4b43c17 | ||
|
b50df10a49 | ||
|
85fe92f604 | ||
|
ee07f637fa | ||
|
321fff24e6 | ||
|
1ef1a1cb22 | ||
|
d7ff8e4be6 | ||
|
f1d00aac0e | ||
|
dc0f3feabf | ||
|
f3086fe49a | ||
|
e9dd7ebdd9 | ||
|
cec7087fc2 | ||
|
67d4c6e271 | ||
|
0728d462a5 | ||
|
be23db5bcf | ||
|
2b33db74e6 | ||
|
37893936b9 | ||
|
bdbd19c3da | ||
|
d87a14db0d | ||
|
eb526f5b67 | ||
|
ca3f5c599a | ||
|
3e29ed4376 | ||
|
55d99b24f4 | ||
|
0cc7ed1647 | ||
|
4fedc30301 | ||
|
da5ae02a06 | ||
|
1283e86ce8 | ||
|
8a1c57c434 | ||
|
267a0bbe4d | ||
|
5cac4f52bc | ||
|
4334ae8c9d | ||
|
8be29062f6 | ||
|
64a00481f0 | ||
|
ba1bcf226a | ||
|
5f944d8e4a | ||
|
42c0d1002f | ||
|
e4dab5fd0d | ||
|
232fd33ec8 | ||
|
979ae73d8d | ||
|
b59cbcac4c | ||
|
cddade059f | ||
|
a08f6a33ac | ||
|
77a4d23301 | ||
|
9484acdc6d | ||
|
6101548d89 | ||
|
620bd1ab2c | ||
|
c17f006992 | ||
|
ce0a52c227 | ||
|
3d1c2dc05a | ||
|
9129ceede1 | ||
|
0623ee0bf2 | ||
|
7716fe4b62 | ||
|
5bf943ce77 | ||
|
92485a02cf | ||
|
c1d9b2c9ed | ||
|
27ccdf165c | ||
|
74dda8ab69 | ||
|
abfaf73a25 | ||
|
be2aa62efc | ||
|
34dc5a0bc6 | ||
|
61809aad21 | ||
|
97e6ad6700 | ||
|
0690f088b6 | ||
|
227575043b | ||
|
4ec92ddf58 | ||
|
7e506c5cbd | ||
|
3e54e75040 | ||
|
702f4fe4c0 | ||
|
318749e5a8 | ||
|
d416deec5e | ||
|
3a569ec0d6 | ||
|
47008c015e | ||
|
5159f69a59 | ||
|
f6554a1082 | ||
|
efa1e52ce1 | ||
|
d6ec0d22d3 | ||
|
d533894a74 | ||
|
d3544e2d97 | ||
|
d888b694cc | ||
|
3b278d0498 | ||
|
76261de805 | ||
|
ca56a78814 | ||
|
5cf5e0ce2b | ||
|
ac7fa164b6 | ||
|
e7b0f859a5 | ||
|
e6264948b1 | ||
|
8d2baa8d1a | ||
|
1147a21dde | ||
|
c1e202b7b1 | ||
|
a910ec1639 | ||
|
0afdbf7040 | ||
|
085a4d41fd | ||
|
39a7b95e32 | ||
|
7e9506874c | ||
|
c559b2104b | ||
|
7712bd685b | ||
|
238f333b81 | ||
|
407f46f248 | ||
|
40e2ba196d | ||
|
eb895ceb58 | ||
|
cbc80391d8 | ||
|
09da69c24a | ||
|
a5779095f8 | ||
|
1880022971 | ||
|
b04016f30f | ||
|
9c14bd9c90 | ||
|
0172d7dc3b | ||
|
6bca3469b6 | ||
|
1fa03e44df | ||
|
9f9584633c | ||
|
6af9b5d0fc | ||
|
f16514db21 | ||
|
a8ef43ca84 | ||
|
dcd44f283f | ||
|
e3ee66527a | ||
|
b239edb266 | ||
|
c2fba35360 | ||
|
281b8580cd | ||
|
f103a919f2 | ||
|
94333a7438 | ||
|
2159e70399 | ||
|
fdf4b901b8 | ||
|
2260f772a7 | ||
|
a6effe0a29 | ||
|
9b985a2a0c | ||
|
83089f1197 | ||
|
c07d54f4c3 | ||
|
8b5055c7ef | ||
|
9330a3ec89 | ||
|
c44d453f20 | ||
|
4d467e9f86 | ||
|
d26502a724 | ||
|
5a2b3a303a | ||
|
6868128267 | ||
|
61180a6e64 | ||
|
856b643c6a | ||
|
07e6438902 | ||
|
16a989c19b | ||
|
f6b3b9cfd4 | ||
|
bcc86d6041 | ||
|
dd45996f9c | ||
|
4c1dd914eb | ||
|
922ed155ed | ||
|
7bee3853a1 | ||
|
270aaf09fc | ||
|
f8d8dae9d7 | ||
|
5c9da4a725 | ||
|
cf8e6e2a76 | ||
|
6e241709b1 | ||
|
81160b6dc7 | ||
|
7697ed053e | ||
|
01c128e413 | ||
|
fab5c17dc2 | ||
|
1b89adc604 | ||
|
e3223164b6 | ||
|
0c38f3b55c | ||
|
d615a7c6a5 | ||
|
7a3921947a | ||
|
1cceed2fd6 | ||
|
a7cf071971 | ||
|
e2295d2419 | ||
|
473a32b57f | ||
|
aa9c3b9f4f | ||
|
b03ee5bd53 | ||
|
c9da38c7ec | ||
|
000be7aa49 | ||
|
1264b286b2 | ||
|
26f21fe6e4 | ||
|
88a72e0153 | ||
|
3e1f10491a | ||
|
dd0391687f | ||
|
311c74584c | ||
|
ac911cff4b | ||
|
cbf786fd56 | ||
|
e45198e403 | ||
|
23c74bd7aa | ||
|
8f0947aa9c | ||
|
edfdc3bf47 | ||
|
dfb02af0f3 | ||
|
d464f94e0f | ||
|
8afa94aae3 | ||
|
6b8ef48e7f | ||
|
4204b9ebf7 | ||
|
3e19234edc | ||
|
bacd260547 | ||
|
9f09dc9df2 | ||
|
469cac9656 | ||
|
01ce94585a | ||
|
a58d6353cd | ||
|
fd4f09588e | ||
|
6977963d01 | ||
|
2905a5570c | ||
|
f4ed527fe8 | ||
|
a8b91561dc | ||
|
b7ac3b8fad | ||
|
247d508d09 | ||
|
0d2b190222 | ||
|
417feec56b | ||
|
dab3f1f844 | ||
|
e190c3b8d6 | ||
|
eec15b38bb | ||
|
44c2276952 | ||
|
2f98bbaa0e | ||
|
c5f3cfe87c | ||
|
8052877a7b | ||
|
fffd36267d | ||
|
02ff6fb5bf | ||
|
c435b7451e | ||
|
1527134eeb | ||
|
46666d17dc | ||
|
50e994c674 | ||
|
80a698d891 | ||
|
46cd387853 | ||
|
ebf217057d | ||
|
9401662f83 | ||
|
2b13b764b4 | ||
|
4fd453ec05 | ||
|
9e42d9d05b | ||
|
5f562f49c3 | ||
|
ba2d7958ce | ||
|
eb6cfdf53d | ||
|
8bcfc3cbee | ||
|
b8ffe5038c | ||
|
25f4adc7ad | ||
|
918a37173e | ||
|
d9b5642078 | ||
|
304675bc74 | ||
|
59d60f54fe | ||
|
100198b55c | ||
|
d43633e936 | ||
|
f4880cc1d0 | ||
|
267d0a2cd1 | ||
|
ba038dc75a | ||
|
3d9e2ad49a | ||
|
faa4c7c08e | ||
|
395d66e648 | ||
|
6fd119c199 | ||
|
5bd5245044 | ||
|
321fc3184a | ||
|
950c5ace3f | ||
|
3f20d5b0d1 | ||
|
e9ec5f26aa | ||
|
ea576477cd | ||
|
9e35f96ce5 | ||
|
96cd097af8 | ||
|
2230f94410 | ||
|
22dff82bd3 | ||
|
9c9699d2d1 | ||
|
b04c438d6f | ||
|
9aeb18df37 | ||
|
5735e9da36 | ||
|
cd482ef1d2 | ||
|
cf9467419d | ||
|
d04df35506 | ||
|
bf7083effc | ||
|
2d93be519f | ||
|
10e40930e2 | ||
|
c7b3e3cd44 | ||
|
47555d5955 | ||
|
9a5300dd2d | ||
|
5839e0b270 | ||
|
9063ba2a1f | ||
|
59256f4cb9 | ||
|
9d6b597552 | ||
|
ffcfb7495b | ||
|
1ed881e0bf | ||
|
ce645a83ab | ||
|
f8cbf41c3a | ||
|
047337ae81 | ||
|
11fb0aa1d8 | ||
|
4923ce8b83 | ||
|
cb03580421 | ||
|
13d3227323 | ||
|
120a488121 | ||
|
b8873d1277 | ||
|
471ff2bb01 | ||
|
aeb6ad50ff | ||
|
1ac108a9b8 | ||
|
645af32b4c | ||
|
0b13e22574 | ||
|
90176da888 | ||
|
4077db37e3 | ||
|
236b1ed099 | ||
|
588e5566d0 | ||
|
b7f1461405 | ||
|
5295f923b5 | ||
|
bc67817d64 | ||
|
679b5b6b79 | ||
|
b3aa8975e9 | ||
|
ee3097e6b0 | ||
|
e342096029 | ||
|
12da6ddd6c | ||
|
8f21ea8385 | ||
|
ed39043bbe | ||
|
276b404dd9 | ||
|
858a513569 | ||
|
d1c22b122c | ||
|
cc1eab653a | ||
|
4b77e8c577 | ||
|
8a1376e966 | ||
|
dbb35d102f | ||
|
5fd82c1188 | ||
|
ab631c5dc4 | ||
|
9e6fb755d9 | ||
|
fd5b598b63 | ||
|
540e5783b6 | ||
|
ea04dd5303 | ||
|
6a90655c44 | ||
|
b35207a578 | ||
|
cbe93d7164 | ||
|
2de2d114a1 | ||
|
1a1ab30574 | ||
|
5d8d729bd7 | ||
|
6dcb0d3750 | ||
|
9cd3ff1d31 | ||
|
acbe4f89a6 | ||
|
5565ddd3f4 | ||
|
e1f0192e61 | ||
|
474323b95f | ||
|
9aed9713c8 | ||
|
769d9cf314 | ||
|
870f16f9c6 | ||
|
54e300443b | ||
|
fac6574cb5 | ||
|
1523758106 | ||
|
701710a2a1 | ||
|
728c2ee092 | ||
|
faff263b80 | ||
|
62627ba1b0 | ||
|
a1022f768b | ||
|
1bf4475e1b | ||
|
f67ca05147 | ||
|
796aaf82ce | ||
|
6d15921674 | ||
|
4f1cab5afe | ||
|
4c4f310b79 | ||
|
dd428bc1e1 | ||
|
d32df13f1b | ||
|
8af8ccd54b | ||
|
28d78a7988 | ||
|
4f4d2532b7 | ||
|
bc19cbd525 | ||
|
17261dfab6 | ||
|
c8e2416e08 | ||
|
177f0eb053 | ||
|
b4fa38bf8c | ||
|
a5cb93541a | ||
|
595c5ca64d | ||
|
7c44d747de | ||
|
2814a95a74 | ||
|
4d18e0ceb4 | ||
|
d4bc999c54 | ||
|
36f0cd1a23 | ||
|
343ad6ea96 | ||
|
a7b09e91ba | ||
|
428625e61b | ||
|
b18b2fe0e3 | ||
|
a3165a0540 | ||
|
d0aaa0620d | ||
|
0c3b9ccfbf | ||
|
1110f4fb7f | ||
|
796a9418d3 | ||
|
3506063e65 | ||
|
2f9cc393a7 | ||
|
96545c618a | ||
|
d93b30f982 | ||
|
35b37a6a88 | ||
|
a1d52b4265 | ||
|
ce0a72c6ce | ||
|
b509471140 | ||
|
451fa7dcd7 | ||
|
aa78bf44ef | ||
|
201fcf6afa | ||
|
b57a9957a3 | ||
|
2ee62c9e9e | ||
|
20ec609535 | ||
|
3edfd5b285 | ||
|
2ea731beeb | ||
|
c9eb5691b5 | ||
|
127a4759ac | ||
|
9b68faac0e | ||
|
4f10dd491c | ||
|
6128f1e431 | ||
|
24dfa2703d | ||
|
54e9e5656b | ||
|
00dd5a8c84 | ||
|
7cc1e5b2c6 | ||
|
79935593e2 | ||
|
6e7f8507b1 | ||
|
849e68ece2 | ||
|
db40681be7 | ||
|
9c4c6f2d53 | ||
|
5cb58d98a0 | ||
|
1885ea42a4 | ||
|
62cc1f73ca | ||
|
9bc79c3ed3 | ||
|
867f783d5e | ||
|
f2dca82642 | ||
|
cfc156d022 | ||
|
a66d42e158 | ||
|
679c8a5ec5 | ||
|
a6786c7494 | ||
|
a23e72454d | ||
|
85edd47a49 | ||
|
99cc1b488f | ||
|
1466d7481c | ||
|
39539214df | ||
|
498f23b5fc | ||
|
fe573893a1 | ||
|
cc44bfac77 | ||
|
43aad4ab14 | ||
|
8aeaf292b3 | ||
|
4fcbc146ca | ||
|
9bf34a5117 | ||
|
06058cfc24 | ||
|
664a5b6b91 | ||
|
f57c01dee8 | ||
|
8c1e01566a | ||
|
4d4f845b9e | ||
|
aff912da55 | ||
|
2d8997c6d7 | ||
|
b56e4b5674 | ||
|
627db30410 | ||
|
d2b122efe5 | ||
|
4ec5ad9e33 | ||
|
0c279750be | ||
|
5674b99734 | ||
|
ad39b43df3 | ||
|
03a0402b3e | ||
|
78d4985d10 | ||
|
f044b3d249 | ||
|
23bf78a026 | ||
|
0229f09ec6 | ||
|
e50aad0ea0 | ||
|
52b1c6733b | ||
|
5f6d9e060c | ||
|
ab7b06f71e | ||
|
8cef4f6e90 | ||
|
2c4c98b0e5 | ||
|
8e136cebe8 | ||
|
0fd743bcac | ||
|
bd4cf5d7f7 | ||
|
0411d5e493 | ||
|
58482f8ae6 | ||
|
003bdf18cb | ||
|
e0d496c7b7 | ||
|
50ee2e9810 | ||
|
a95d592659 | ||
|
25d1e575ef | ||
|
4606173ed1 | ||
|
954b87db41 | ||
|
cd8501d80e | ||
|
cf811c547b | ||
|
973b3405ef | ||
|
fd931c4884 | ||
|
8edd29abaa | ||
|
3b6daa99c0 | ||
|
6e868408ed | ||
|
d1f1c8f470 | ||
|
d2bab4e7ef | ||
|
dfc57d0529 | ||
|
de1768b7bb | ||
|
c025d1798b | ||
|
3a82481d70 | ||
|
c1da898c59 | ||
|
293c57568e | ||
|
8cb32c34b5 | ||
|
9a8b10bbf5 | ||
|
cd2d090a8b | ||
|
841bf29764 | ||
|
a6ba8d41b9 | ||
|
945ff446a9 | ||
|
e0ac8abd51 | ||
|
bc88a7d3ed | ||
|
ce0c7ea3eb | ||
|
8aa34cecef | ||
|
a5260c0831 | ||
|
4758820aa4 | ||
|
44eb25e9f6 | ||
|
46996f7049 | ||
|
8db72d2dfd | ||
|
bed45a8310 | ||
|
ffe03d40f5 | ||
|
91148035ec | ||
|
cf06f29ef8 | ||
|
8da5bf9f8e | ||
|
cb19721dca | ||
|
2922ae4603 | ||
|
e655edd9bd | ||
|
b141da1475 | ||
|
543397efe4 | ||
|
9c620de649 | ||
|
3a00ecab8f | ||
|
5605e63e5f | ||
|
62a6eabc88 | ||
|
0e32238505 | ||
|
66490c9083 | ||
|
6b4d064f19 | ||
|
189dba2e21 | ||
|
3b5f105daf | ||
|
6d3cbc573c | ||
|
0c5e5ef578 | ||
|
271300aa8d | ||
|
1440faf825 | ||
|
46978d26f9 | ||
|
a24ca078e6 | ||
|
fea1de36a4 | ||
|
f085393730 | ||
|
77d14ee7ce | ||
|
989528f729 | ||
|
3751b0e2bc | ||
|
7ae66a63a4 | ||
|
9c4a0245a3 | ||
|
c1be97e366 | ||
|
04448c3c50 | ||
|
cd2873e9d3 | ||
|
4b89e08ea6 | ||
|
bede7abdd8 | ||
|
10bd073b7d | ||
|
7d2cc66f11 | ||
|
82073098e0 | ||
|
a6ad1674e3 | ||
|
f66e69cb75 | ||
|
4d0220e118 | ||
|
71eb3f3b69 | ||
|
83159a121f | ||
|
c8692a08ce | ||
|
fa3ffe4d7a | ||
|
59b391e983 | ||
|
8d45338044 | ||
|
2b30bea3e0 | ||
|
ec02b11b7f | ||
|
622e0c6130 | ||
|
613ff5d463 | ||
|
85efe208b3 | ||
|
04d3cf8593 | ||
|
5069099000 | ||
|
95ec5dc3f6 | ||
|
0a5dffd034 | ||
|
cd77af318d | ||
|
d674ac9e0c | ||
|
4c55b5657f | ||
|
7592a71bd5 | ||
|
2b2c60cbe3 | ||
|
3c9d477c06 | ||
|
1c20b33d4e | ||
|
925545d74f | ||
|
d1ef4a7342 | ||
|
7cd7d22e8b | ||
|
0af3657acb | ||
|
0b749a4cab | ||
|
b4f2b005e1 | ||
|
7bd524ba55 | ||
|
99bd9d88d9 | ||
|
99926a89b2 | ||
|
8f1aea1c43 | ||
|
8f6c65384a | ||
|
746f1029a8 | ||
|
a984151912 | ||
|
fe27169745 | ||
|
398326b3aa | ||
|
2e4b03efff | ||
|
d29dee3bd4 | ||
|
19ae575b4f | ||
|
1e42de9155 | ||
|
5c0b5ef74b | ||
|
c2f62e2610 | ||
|
16397fe978 | ||
|
9b69910cb8 | ||
|
57194fd0a1 | ||
|
94e564f135 | ||
|
79701fe227 | ||
|
76a21cafa0 | ||
|
3ce5c2ec45 | ||
|
c6ed2c5c57 | ||
|
7a8765826e | ||
|
6295c8275e | ||
|
c19201b636 | ||
|
ea009d912e | ||
|
998e051a45 | ||
|
4069e87872 | ||
|
fafaabb6e7 | ||
|
79e530f0e6 | ||
|
6e847b5f58 | ||
|
30cf078df5 | ||
|
4e09246c7f | ||
|
ca628d9a90 | ||
|
08ca95b143 | ||
|
22cb41dc29 | ||
|
e37ab7d8ad | ||
|
6e5fa2c514 | ||
|
6815fe7a0a | ||
|
66b90be0da | ||
|
93b7b7bc91 | ||
|
9b6be3466b | ||
|
f0d277af32 | ||
|
364c1d3e98 | ||
|
61c8830e24 | ||
|
0c72378001 | ||
|
d9620c373f | ||
|
b934d3404f | ||
|
b79d76fc2e | ||
|
f8bab9ce6b | ||
|
f03f8076f3 | ||
|
4f4f499d47 | ||
|
f11e936143 | ||
|
8e52b8597c | ||
|
d7f00b505a | ||
|
3697ddabfa | ||
|
3df32de83b | ||
|
1251cbdc76 | ||
|
0a04f5d631 | ||
|
c8ba3d8ab9 | ||
|
6a2326c4b3 | ||
|
517c0e86cb | ||
|
3836d5037c | ||
|
006195e8cc | ||
|
6c6789411a | ||
|
f9f6abe7b3 | ||
|
84bd5eff50 | ||
|
78d888ffaf | ||
|
405f7b1137 | ||
|
20e0a2553e | ||
|
30c1825abd | ||
|
09d5e0b689 | ||
|
2c862fe7e7 | ||
|
676dbcc9d0 | ||
|
5dc2f0ac47 | ||
|
341b048d6c | ||
|
415d8f9466 | ||
|
5e8951fdaa | ||
|
d44e449e92 | ||
|
e7b5bb261d | ||
|
56287a2958 | ||
|
06f6b3153c | ||
|
f7daa6e3f5 | ||
|
a43afbc124 | ||
|
dab74460bb | ||
|
7e6a8b477d | ||
|
b9d9a78868 | ||
|
d31021c02d | ||
|
cb1dcb5786 | ||
|
5028a54422 | ||
|
94317be1ae | ||
|
110511461a | ||
|
16daf7332a | ||
|
d7a74baa9d | ||
|
603fc0a591 | ||
|
f7c4592944 | ||
|
69ff15ff71 | ||
|
3c099ea783 | ||
|
af260ad229 |
@ -5,11 +5,13 @@ JITSI_PRIVATE_MODE=false
|
|||||||
JITSI_ISS=
|
JITSI_ISS=
|
||||||
SECRET_JITSI_KEY=
|
SECRET_JITSI_KEY=
|
||||||
ADMIN_API_TOKEN=123
|
ADMIN_API_TOKEN=123
|
||||||
START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json
|
START_ROOM_URL=/_/global/maps.workadventure.localhost/starter/map.json
|
||||||
# If your Turn server is configured to use the Turn REST API, you should put the shared auth secret here.
|
# If your Turn server is configured to use the Turn REST API, you should put the shared auth secret here.
|
||||||
# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file.
|
# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file.
|
||||||
# Keep empty if you are sharing hard coded / clear text credentials.
|
# Keep empty if you are sharing hard coded / clear text credentials.
|
||||||
TURN_STATIC_AUTH_SECRET=
|
TURN_STATIC_AUTH_SECRET=
|
||||||
|
DISABLE_NOTIFICATIONS=true
|
||||||
|
SKIP_RENDER_OPTIMIZATIONS=false
|
||||||
|
|
||||||
# The email address used by Let's encrypt to send renewal warnings (compulsory)
|
# The email address used by Let's encrypt to send renewal warnings (compulsory)
|
||||||
ACME_EMAIL=
|
ACME_EMAIL=
|
||||||
@ -17,3 +19,13 @@ ACME_EMAIL=
|
|||||||
MAX_PER_GROUP=4
|
MAX_PER_GROUP=4
|
||||||
MAX_USERNAME_LENGTH=8
|
MAX_USERNAME_LENGTH=8
|
||||||
|
|
||||||
|
OPID_CLIENT_ID=
|
||||||
|
OPID_CLIENT_SECRET=
|
||||||
|
OPID_CLIENT_ISSUER=
|
||||||
|
OPID_CLIENT_REDIRECT_URL=
|
||||||
|
OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen
|
||||||
|
OPID_PROFILE_SCREEN_PROVIDER=
|
||||||
|
DISABLE_ANONYMOUS=
|
||||||
|
|
||||||
|
# If you want to have a contact page in your menu, you MUST set CONTACT_URL to the URL of the page that you want
|
||||||
|
CONTACT_URL=
|
70
.github/workflows/build-and-deploy.yml
vendored
70
.github/workflows/build-and-deploy.yml
vendored
@ -1,7 +1,13 @@
|
|||||||
name: Build, push and deploy Docker image
|
name: Build, push and deploy Docker image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
- push
|
push:
|
||||||
|
branches: [master, develop]
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
pull_request:
|
||||||
|
types: [ labeled, synchronize ]
|
||||||
|
|
||||||
|
|
||||||
# Enables BuildKit
|
# Enables BuildKit
|
||||||
env:
|
env:
|
||||||
@ -10,7 +16,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build-front:
|
build-front:
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -30,11 +36,11 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-front
|
repository: thecodingmachine/workadventure-front
|
||||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-back:
|
build-back:
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -53,11 +59,11 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-back
|
repository: thecodingmachine/workadventure-back
|
||||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-pusher:
|
build-pusher:
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -76,11 +82,11 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-pusher
|
repository: thecodingmachine/workadventure-pusher
|
||||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-uploader:
|
build-uploader:
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -99,11 +105,11 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-uploader
|
repository: thecodingmachine/workadventure-uploader
|
||||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
build-maps:
|
build-maps:
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@ -123,7 +129,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
repository: thecodingmachine/workadventure-maps
|
repository: thecodingmachine/workadventure-maps
|
||||||
tags: ${{ env.GITHUB_REF_SLUG }}
|
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
add_git_labels: true
|
add_git_labels: true
|
||||||
|
|
||||||
deeploy:
|
deeploy:
|
||||||
@ -134,6 +140,7 @@ jobs:
|
|||||||
- build-maps
|
- build-maps
|
||||||
- build-uploader
|
- build-uploader
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -142,6 +149,37 @@ jobs:
|
|||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@3.1.0
|
- uses: rlespinasse/github-slug-action@3.1.0
|
||||||
|
|
||||||
|
- name: Write certificate
|
||||||
|
run: echo "${CERTS_PRIVATE_KEY}" > secret.key && chmod 0600 secret.key
|
||||||
|
env:
|
||||||
|
CERTS_PRIVATE_KEY: ${{ secrets.CERTS_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Download certificate
|
||||||
|
run: mkdir secrets && scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i secret.key ubuntu@cert.workadventu.re:./config/live/workadventu.re/* secrets/
|
||||||
|
|
||||||
|
- name: Create namespace
|
||||||
|
uses: steebchen/kubectl@v1.0.0
|
||||||
|
env:
|
||||||
|
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
|
||||||
|
with:
|
||||||
|
args: create namespace workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Delete old certificates in namespace
|
||||||
|
uses: steebchen/kubectl@v1.0.0
|
||||||
|
env:
|
||||||
|
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
|
||||||
|
with:
|
||||||
|
args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} delete secret certificate-tls
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Install certificates in namespace
|
||||||
|
uses: steebchen/kubectl@v1.0.0
|
||||||
|
env:
|
||||||
|
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
|
||||||
|
with:
|
||||||
|
args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} create secret tls certificate-tls --key="secrets/privkey.pem" --cert="secrets/fullchain.pem"
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: thecodingmachine/deeployer-action@master
|
uses: thecodingmachine/deeployer-action@master
|
||||||
env:
|
env:
|
||||||
@ -151,14 +189,16 @@ jobs:
|
|||||||
JITSI_URL: ${{ secrets.JITSI_URL }}
|
JITSI_URL: ${{ secrets.JITSI_URL }}
|
||||||
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
|
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
|
||||||
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
|
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
|
||||||
|
DEPLOY_REF: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
|
POSTHOG_API_KEY: ${{ secrets.POSTHOG_API_KEY }}
|
||||||
|
POSTHOG_URL: ${{ secrets.POSTHOG_URL }}
|
||||||
with:
|
with:
|
||||||
namespace: workadventure-${{ env.GITHUB_REF_SLUG }}
|
namespace: workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
|
||||||
|
|
||||||
- name: Add a comment in PR
|
- name: Add a comment in PR
|
||||||
uses: unsplash/comment-on-pr@v1.2.0
|
uses: unsplash/comment-on-pr@v1.2.0
|
||||||
if: ${{ env.GITHUB_REF_SLUG != 'master' }}
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
msg: Environment deployed at https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
msg: "Environment deployed at https://play-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re \nTests available at https://maps-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re/tests"
|
||||||
check_for_duplicate_msg: true
|
|
||||||
|
10
.github/workflows/cleanup.yml
vendored
10
.github/workflows/cleanup.yml
vendored
@ -1,7 +1,8 @@
|
|||||||
name: Cleanup images and environments
|
name: Cleanup images and environments
|
||||||
|
|
||||||
on:
|
on:
|
||||||
- delete
|
pull_request:
|
||||||
|
types: [ closed ]
|
||||||
|
|
||||||
# Enables BuildKit
|
# Enables BuildKit
|
||||||
env:
|
env:
|
||||||
@ -14,13 +15,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Create a slugified value of the branch
|
# Create a slugified value of the branch
|
||||||
- uses: rlespinasse/github-slug-action@1.1.0
|
- uses: rlespinasse/github-slug-action@3.1.0
|
||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
|
continue-on-error: true
|
||||||
uses: thecodingmachine/deeployer-cleanup-action@master
|
uses: thecodingmachine/deeployer-cleanup-action@master
|
||||||
env:
|
env:
|
||||||
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }}
|
||||||
with:
|
with:
|
||||||
# FIXME: we are not using ${{ env.GITHUB_REF_SLUG }} that resolves to master BUT! we are not using a slugified namespace
|
namespace: workadventure-${{ env.GITHUB_HEAD_REF_SLUG }}
|
||||||
# so complex namespace names will not be treated correctly
|
|
||||||
namespace: workadventure-${{ github.event.ref }}
|
|
||||||
|
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ develop ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ develop ]
|
||||||
|
schedule:
|
||||||
|
- cron: '24 17 * * 0'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
|
# Learn more:
|
||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
|
# and modify them (or add more) to build your code if your project
|
||||||
|
# uses a compiled language
|
||||||
|
|
||||||
|
#- run: |
|
||||||
|
# make bootstrap
|
||||||
|
# make release
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
28
.github/workflows/continuous_integration.yml
vendored
28
.github/workflows/continuous_integration.yml
vendored
@ -3,8 +3,11 @@
|
|||||||
name: "Continuous Integration"
|
name: "Continuous Integration"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
- "pull_request"
|
push:
|
||||||
- "push"
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
@ -46,13 +49,22 @@ jobs:
|
|||||||
- name: "Build"
|
- name: "Build"
|
||||||
run: yarn run build
|
run: yarn run build
|
||||||
env:
|
env:
|
||||||
API_URL: "localhost:8080"
|
PUSHER_URL: "//localhost:8080"
|
||||||
|
ADMIN_URL: "//localhost:80"
|
||||||
|
working-directory: "front"
|
||||||
|
|
||||||
|
- name: "Svelte check"
|
||||||
|
run: yarn run svelte-check
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
- name: "Lint"
|
- name: "Lint"
|
||||||
run: yarn run lint
|
run: yarn run lint
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
|
- name: "Pretty"
|
||||||
|
run: yarn run pretty-check
|
||||||
|
working-directory: "front"
|
||||||
|
|
||||||
- name: "Jasmine"
|
- name: "Jasmine"
|
||||||
run: yarn test
|
run: yarn test
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
@ -69,7 +81,7 @@ jobs:
|
|||||||
- name: "Setup NodeJS"
|
- name: "Setup NodeJS"
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '12.x'
|
node-version: '14.x'
|
||||||
|
|
||||||
- name: Install Protoc
|
- name: Install Protoc
|
||||||
uses: arduino/setup-protoc@v1
|
uses: arduino/setup-protoc@v1
|
||||||
@ -100,6 +112,10 @@ jobs:
|
|||||||
run: yarn test
|
run: yarn test
|
||||||
working-directory: "pusher"
|
working-directory: "pusher"
|
||||||
|
|
||||||
|
- name: "Prettier"
|
||||||
|
run: yarn run pretty-check
|
||||||
|
working-directory: "pusher"
|
||||||
|
|
||||||
continuous-integration-back:
|
continuous-integration-back:
|
||||||
name: "Continuous Integration Back"
|
name: "Continuous Integration Back"
|
||||||
|
|
||||||
@ -143,3 +159,7 @@ jobs:
|
|||||||
run: yarn test
|
run: yarn test
|
||||||
working-directory: "back"
|
working-directory: "back"
|
||||||
|
|
||||||
|
- name: "Prettier"
|
||||||
|
run: yarn run pretty-check
|
||||||
|
working-directory: "back"
|
||||||
|
|
||||||
|
60
.github/workflows/end_to_end_tests.yml
vendored
Normal file
60
.github/workflows/end_to_end_tests.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
|
||||||
|
|
||||||
|
name: "End to end tests"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
end-to-end-tests:
|
||||||
|
name: "End-to-end testcafe tests"
|
||||||
|
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: "Checkout"
|
||||||
|
uses: "actions/checkout@v2.0.0"
|
||||||
|
|
||||||
|
- name: "Setup .env file"
|
||||||
|
run: cp .env.template .env
|
||||||
|
|
||||||
|
- name: "Edit ownership of file for test cases"
|
||||||
|
run: sudo chown 1000:1000 -R .
|
||||||
|
|
||||||
|
- name: "Start environment"
|
||||||
|
run: docker-compose up -d
|
||||||
|
|
||||||
|
- name: "Wait for environment to build (and downloading testcafe image)"
|
||||||
|
run: (docker-compose -f docker-compose.testcafe.yml pull &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully"
|
||||||
|
|
||||||
|
# - name: "temp debug: display logs"
|
||||||
|
# run: docker-compose logs
|
||||||
|
#
|
||||||
|
# - name: "Wait for back start"
|
||||||
|
# run: docker-compose logs -f back | grep -q "WorkAdventure HTTP API starting on port"
|
||||||
|
#
|
||||||
|
# - name: "Wait for pusher start"
|
||||||
|
# run: docker-compose logs -f pusher | grep -q "WorkAdventure starting on port"
|
||||||
|
|
||||||
|
- name: "Run tests"
|
||||||
|
run: docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
|
||||||
|
|
||||||
|
- name: Upload failed tests
|
||||||
|
if: ${{ failure() }}
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: my-artifact
|
||||||
|
path: './tests/screenshots/'
|
||||||
|
|
||||||
|
- name: Display state
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: docker-compose ps
|
||||||
|
|
||||||
|
- name: Display logs
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: docker-compose logs
|
14
.github/workflows/push-to-npm.yml
vendored
14
.github/workflows/push-to-npm.yml
vendored
@ -2,6 +2,7 @@ name: Push @workadventure/iframe-api-typings to NPM
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
push:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -13,10 +14,6 @@ jobs:
|
|||||||
node-version: '14.x'
|
node-version: '14.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- name: Edit tsconfig.json to add declarations
|
|
||||||
run: "sed -i 's/\"declaration\": false/\"declaration\": true/g' tsconfig.json"
|
|
||||||
working-directory: "front"
|
|
||||||
|
|
||||||
- name: Replace version number
|
- name: Replace version number
|
||||||
run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json'
|
run: 'sed -i "s#VERSION_PLACEHOLDER#${GITHUB_REF/refs\/tags\//}#g" package.json'
|
||||||
working-directory: "front/packages/iframe-api-typings"
|
working-directory: "front/packages/iframe-api-typings"
|
||||||
@ -47,15 +44,19 @@ jobs:
|
|||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
- name: "Build"
|
- name: "Build"
|
||||||
run: yarn run build
|
run: yarn run build-typings
|
||||||
env:
|
env:
|
||||||
API_URL: "localhost:8080"
|
PUSHER_URL: "//localhost:8080"
|
||||||
|
ADMIN_URL: "//localhost:80"
|
||||||
working-directory: "front"
|
working-directory: "front"
|
||||||
|
|
||||||
# We build the front to generate the typings of iframe_api, then we copy those typings in a separate package.
|
# We build the front to generate the typings of iframe_api, then we copy those typings in a separate package.
|
||||||
- name: Copy typings to package dir
|
- name: Copy typings to package dir
|
||||||
run: cp front/dist/src/iframe_api.d.ts front/packages/iframe-api-typings/iframe_api.d.ts
|
run: cp front/dist/src/iframe_api.d.ts front/packages/iframe-api-typings/iframe_api.d.ts
|
||||||
|
|
||||||
|
- name: Copy typings to package dir (2)
|
||||||
|
run: cp -R front/dist/src/Api front/packages/iframe-api-typings/Api
|
||||||
|
|
||||||
- name: Install dependencies in package
|
- name: Install dependencies in package
|
||||||
run: yarn install
|
run: yarn install
|
||||||
working-directory: "front/packages/iframe-api-typings"
|
working-directory: "front/packages/iframe-api-typings"
|
||||||
@ -65,3 +66,4 @@ jobs:
|
|||||||
working-directory: "front/packages/iframe-api-typings"
|
working-directory: "front/packages/iframe-api-typings"
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -6,4 +6,6 @@ docker-compose.override.yaml
|
|||||||
*.DS_Store
|
*.DS_Store
|
||||||
maps/yarn.lock
|
maps/yarn.lock
|
||||||
maps/dist/computer.js
|
maps/dist/computer.js
|
||||||
maps/dist/computer.js.map
|
maps/dist/computer.js.map
|
||||||
|
node_modules
|
||||||
|
_
|
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
_
|
15
.husky/pre-commit
Executable file
15
.husky/pre-commit
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd front || exit
|
||||||
|
yarn run precommit
|
||||||
|
)
|
||||||
|
(
|
||||||
|
cd pusher || exit
|
||||||
|
yarn run precommit
|
||||||
|
)
|
||||||
|
(
|
||||||
|
cd back || exit
|
||||||
|
yarn run precommit
|
||||||
|
)
|
166
CHANGELOG.md
Normal file
166
CHANGELOG.md
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
## Version develop
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
- Added multi Co-Website management
|
||||||
|
|
||||||
|
### Bugfix
|
||||||
|
- Moving a discussion over a user will now add this user to the discussion
|
||||||
|
- Being in a silent zone new forces mediaConstraints to false (#1508)
|
||||||
|
- Fixes for the emote menu (#1501)
|
||||||
|
- Fixing chat message attributed to wrong user (#1507 #1528)
|
||||||
|
|
||||||
|
## Version 1.5.0
|
||||||
|
### Updates
|
||||||
|
- Added support for login with OpenID Connect
|
||||||
|
- New scripting library available to extend WorkAdventure: see [Scripting API Extra](https://github.com/workadventure/scripting-api-extra/)
|
||||||
|
- New menu design!
|
||||||
|
- New `openTab` property (#1419)
|
||||||
|
- Possible integration with Posthog (#1458)
|
||||||
|
|
||||||
|
### Bugfix
|
||||||
|
- Fixing layers flattened several times (#1427 @Lurkars)
|
||||||
|
- Fixing CSS of video elements
|
||||||
|
- Chat now scrolls to bottom when opened (#1450)
|
||||||
|
- Fixing silent zone not respected when exiting from Jitsi (#1456)
|
||||||
|
- Fixing "yarn install" failing because of missing rights on some Docker installs (#1457)
|
||||||
|
- Fixing audio not shut down when exiting a room (#1459)
|
||||||
|
|
||||||
|
### Misc
|
||||||
|
- Finished migrating "Build your map" documentation into the "/docs" directory of this repository (#1417 #1385)
|
||||||
|
- Refactoring documentation (dedicated page for variables) (#1414)
|
||||||
|
- Front container code is now completely linted (#1413)
|
||||||
|
|
||||||
|
## Version 1.4.15
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
- New scripting API features :
|
||||||
|
- Use `WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu` to add a custom menu or an iframe to the menu.
|
||||||
|
- New `jitsiWidth` parameter to set the width of Jitsi and Cowebsite (#1398 @tabascoeye)
|
||||||
|
- Refactored the way videos are displayed to better cope for vertical videos (on mobile)
|
||||||
|
- Fixing reconnection issues after 5 minutes of an inactive tab on Google Chrome
|
||||||
|
- Changes performed in `WA.room.setPropertyLayer` now have a real-time impact (#1395)
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
- Fixing streams in bubbles sometimes improperly muted when there are more than 2 people in the bubble (#1400 #1402)
|
||||||
|
- Properly displaying carriage returns in popups (#1388)
|
||||||
|
- `WA.state` now answers correctly to "in" keyword (#1393)
|
||||||
|
- Variables can now be nested in group layers (#1406)
|
||||||
|
|
||||||
|
## Version 1.4.14
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
- New scripting API features :
|
||||||
|
- Use `WA.room.loadTileset(url: string) : Promise<number>` to load a tileset from a JSON file.
|
||||||
|
- Rewrote the way authentification works: the auth jwt token can now contains an email instead of an uuid
|
||||||
|
- Added an OpenId login flow than can be plugged to any OIDC provider.
|
||||||
|
- You can send a message to all rooms of your world from the console global message (user with tag admin only).
|
||||||
|
|
||||||
|
## Version 1.4.11
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
- Added the ability to have animated tiles in maps #1216 #1217
|
||||||
|
- Enabled outlines on actionable item again (they were disabled when migrating to Phaser 3.50) #1218
|
||||||
|
- Enabled outlines on player names (when the mouse hovers on a player you can interact with) #1219
|
||||||
|
- Migrated the admin console to Svelte, and redesigned the console #1211
|
||||||
|
- Layer properties (like `exitUrl`, `silent`, etc...) can now also used in tile properties #1210 (@jonnytest1)
|
||||||
|
- New scripting API features :
|
||||||
|
- Use `WA.onInit(): Promise<void>` to wait for scripting API initialization
|
||||||
|
- Use `WA.room.showLayer(): void` to show a layer
|
||||||
|
- Use `WA.room.hideLayer(): void` to hide a layer
|
||||||
|
- Use `WA.room.setProperty() : void` to add, delete or change existing property of a layer
|
||||||
|
- Use `WA.player.onPlayerMove(): void` to track the movement of the current player
|
||||||
|
- Use `WA.player.id: string|undefined` to get the ID of the current player
|
||||||
|
- Use `WA.player.name: string` to get the name of the current player
|
||||||
|
- Use `WA.player.tags: string[]` to get the tags of the current player
|
||||||
|
- Use `WA.room.id: string` to get the ID of the room
|
||||||
|
- Use `WA.room.mapURL: string` to get the URL of the map
|
||||||
|
- Use `WA.room.mapURL: string` to get the URL of the map
|
||||||
|
- Use `WA.room.getMap(): Promise<ITiledMap>` to get the JSON map file
|
||||||
|
- Use `WA.room.setTiles(): void` to add, delete or change an array of tiles
|
||||||
|
- Use `WA.ui.registerMenuCommand(): void` to add a custom menu
|
||||||
|
- Use `WA.state.loadVariable(key: string): unknown` to retrieve a variable
|
||||||
|
- Use `WA.state.saveVariable(key: string, value: unknown): Promise<void>` to set a variable (across the room, for all users)
|
||||||
|
- Use `WA.state.onVariableChange(key: string): Observable<unknown>` to track a variable
|
||||||
|
- Use `WA.state.[any variable]: unknown` to access directly any variable (this is a shortcut to using `WA.state.loadVariable` and `WA.state.saveVariable`)
|
||||||
|
- Users blocking now relies on UUID rather than ID. A blocked user that leaves a room and comes back will stay blocked.
|
||||||
|
- The text chat was redesigned to be prettier and to use more features :
|
||||||
|
- The chat is now persistent between discussions and always accessible
|
||||||
|
- The chat now tracks incoming and outcoming users in your conversation
|
||||||
|
- The chat allows your to see the visit card of users
|
||||||
|
- You can close the chat window with the escape key
|
||||||
|
- Added a 'Enable notifications' button in the menu.
|
||||||
|
- The exchange format between Pusher and Admin servers has changed. If you have your own implementation of an admin server, these endpoints signatures have changed:
|
||||||
|
- `/api/map`: now accepts a complete room URL instead of organization/world/room slugs
|
||||||
|
- `/api/ban`: new endpoint to report users
|
||||||
|
- as a side effect, the "routing" is now completely stored on the admin side, so by implementing your own admin server, you can develop completely custom routing
|
||||||
|
|
||||||
|
## Version 1.4.3 - 1.4.4 - 1.4.5
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- Fixing the generation of @workadventure/iframe-api-typings
|
||||||
|
|
||||||
|
## Version 1.4.2
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
- A script in an iframe opened by another script can use the IFrame API.
|
||||||
|
|
||||||
|
## Version 1.4.1
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Loading errors after the preload stage should not crash the game anymore
|
||||||
|
|
||||||
|
## Version 1.4.0
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
- Scripting API:
|
||||||
|
- Changed function names: `restorePlayerControl` => `restorePlayerControls`, `disablePlayerControl` => `disablePlayerControls`.
|
||||||
|
Please keep in mind that the scripting API is still experimental. Some breaking changes can occur in it until we mark it as stable.
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
- Added the emote feature to WorkAdventure. (@Kharhamel, @Tabascoeye)
|
||||||
|
- The emote menu can be opened by clicking on your character.
|
||||||
|
- Clicking on one of its element will close the menu and play an emote above your character.
|
||||||
|
- This emote can be seen by other players.
|
||||||
|
- Player names were improved. (@Kharhamel)
|
||||||
|
- We now create a GameObject.Text instead of GameObject.BitmapText
|
||||||
|
- now use the 'Press Start 2P' font family and added an outline
|
||||||
|
- As a result, we can now allow non-standard letters like french accents or chinese characters!
|
||||||
|
|
||||||
|
- Added the contact card feature. (@Kharhamel)
|
||||||
|
- Click on another player to see its contact info.
|
||||||
|
- Premium-only feature unfortunately. I need to find a way to make it available for all.
|
||||||
|
- If no contact data is found (either because the user is anonymous or because no admin backend), display an error card.
|
||||||
|
|
||||||
|
- Mobile support has been improved
|
||||||
|
- WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used
|
||||||
|
- Mouse wheel support to zoom in / out
|
||||||
|
- Pinch support on mobile to zoom in / out
|
||||||
|
- Improved virtual joystick size (adapts to the zoom level)
|
||||||
|
- Redesigned intermediate scenes
|
||||||
|
- Redesigned Select Companion scene
|
||||||
|
- Redesigned Enter Your Name scene
|
||||||
|
- Added a new `DISPLAY_TERMS_OF_USE` environment variable to trigger the display of terms of use
|
||||||
|
- New scripting API features:
|
||||||
|
- Use `WA.loadSound(): Sound` to load / play / stop a sound
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Pinch gesture does no longer move the character
|
||||||
|
|
||||||
|
## Version 1.3.0
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
* Maps can now contain "group" layers (layers that contain other layers) - #899 #779 (@Lurkars @moufmouf)
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
67
CONTRIBUTING.md
Normal file
67
CONTRIBUTING.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Contributing to WorkAdventure
|
||||||
|
|
||||||
|
Are you looking to help on WorkAdventure? Awesome, feel welcome and read the following sections in order to know how to
|
||||||
|
ask questions and how to work on something.
|
||||||
|
|
||||||
|
## Contributions we are seeking
|
||||||
|
|
||||||
|
We love to receive contributions from our community — you!
|
||||||
|
|
||||||
|
There are many ways to contribute, from writing tutorials or blog posts, improving the documentation,
|
||||||
|
submitting bug reports and feature requests or writing code which can be incorporated into WorkAdventure itself.
|
||||||
|
|
||||||
|
## Using the issue tracker
|
||||||
|
|
||||||
|
First things first: **Do NOT report security vulnerabilities in public issues!**.
|
||||||
|
Please read the [security guide](SECURITY.md) to learn who to do a security disclosure to the WorkAdventure core team.
|
||||||
|
|
||||||
|
You can use [GitHub issue tracker](https://github.com/thecodingmachine/workadventure/issues) to:
|
||||||
|
|
||||||
|
- File bug reports
|
||||||
|
- Ask for feature requests
|
||||||
|
|
||||||
|
If you have more general questions, a good place to ask is [our Discord server](https://discord.gg/YGtngdh9gt).
|
||||||
|
|
||||||
|
Finally, you can come and talk to the WorkAdventure core team... on WorkAdventure, of course! [Our offices are here](https://play.staging.workadventu.re/@/tcm/workadventure/wa-village).
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
Good pull requests - patches, improvements, new features - are a fantastic help. They should remain focused in scope
|
||||||
|
and avoid containing unrelated commits.
|
||||||
|
|
||||||
|
Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code),
|
||||||
|
otherwise you risk spending a lot of time working on something that the project's developers might not want to merge
|
||||||
|
into the project.
|
||||||
|
|
||||||
|
You can ask us on [Discord](https://discord.gg/YGtngdh9gt) or in the [GitHub issues](https://github.com/thecodingmachine/workadventure/issues).
|
||||||
|
|
||||||
|
### Linting your code
|
||||||
|
|
||||||
|
Before committing, be sure to install the "Prettier" precommit hook that will reformat your code to our coding style.
|
||||||
|
|
||||||
|
In order to enable the "Prettier" precommit hook, at the root of the project, run:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ yarn install
|
||||||
|
$ yarn run prepare
|
||||||
|
```
|
||||||
|
|
||||||
|
If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need
|
||||||
|
to run code linting manually:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ docker-compose exec front yarn run pretty
|
||||||
|
$ docker-compose exec pusher yarn run pretty
|
||||||
|
$ docker-compose exec back yarn run pretty
|
||||||
|
```
|
||||||
|
|
||||||
|
### Providing tests
|
||||||
|
|
||||||
|
WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test.
|
||||||
|
|
||||||
|
Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine).
|
||||||
|
|
||||||
|
If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain
|
||||||
|
some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file
|
||||||
|
to add a reference to your newly created test map.
|
||||||
|
|
BIN
README-INTRO.jpg
BIN
README-INTRO.jpg
Binary file not shown.
Before Width: | Height: | Size: 386 KiB |
1
README-LOGO.svg
Normal file
1
README-LOGO.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 16 KiB |
BIN
README-MAP.png
Normal file
BIN
README-MAP.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
19
README.md
19
README.md
@ -1,17 +1,18 @@
|
|||||||
 [](https://discord.gg/YGtngdh9gt)
|
 [](https://discord.gg/YGtngdh9gt)
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
Demo here : [https://workadventu.re/](https://workadventu.re/).
|
Live demo [here](https://play.workadventu.re/@/tcm/workadventure/wa-village).
|
||||||
|
|
||||||
# Work Adventure
|
# WorkAdventure
|
||||||
|
|
||||||
Work Adventure is a web-based collaborative workspace for small to medium teams (2-100 people) presented in the form of a
|
WorkAdventure is a web-based collaborative workspace presented in the form of a
|
||||||
16-bit video game.
|
16-bit video game.
|
||||||
|
|
||||||
In Work Adventure, you can move around your office and talk to your colleagues (using a video-chat feature that is
|
In WorkAdventure you can move around your office and talk to your colleagues (using a video-chat system, triggered when you approach someone).
|
||||||
triggered when you move next to a colleague).
|
|
||||||
|
|
||||||
|
See more features for your virtual office: https://workadventu.re/virtual-office
|
||||||
|
|
||||||
## Setting up a development environment
|
## Setting up a development environment
|
||||||
|
|
||||||
@ -20,7 +21,8 @@ Install Docker.
|
|||||||
Run:
|
Run:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose up
|
cp .env.template .env
|
||||||
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The environment will start.
|
The environment will start.
|
||||||
@ -35,6 +37,9 @@ Note: on some OSes, you will need to add this line to your `/etc/hosts` file:
|
|||||||
127.0.0.1 workadventure.localhost
|
127.0.0.1 workadventure.localhost
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note: If on the first run you get a page with "network error". Try to ``docker-compose stop`` , then ``docker-compose start``.
|
||||||
|
Note 2: If you are still getting "network error". Make sure you are authorizing the self-signed certificate by entering https://pusher.workadventure.localhost and accepting them.
|
||||||
|
|
||||||
### MacOS developers, your environment with Vagrant
|
### MacOS developers, your environment with Vagrant
|
||||||
|
|
||||||
If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c).
|
If you are using MacOS, you can increase Docker performance using Vagrant. If you want more explanations, you can read [this medium article](https://medium.com/better-programming/vagrant-to-increase-docker-performance-with-macos-25b354b0c65c).
|
||||||
|
20
SECURITY.md
Normal file
20
SECURITY.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
First things first: **Do NOT report security vulnerabilities in public issues!**
|
||||||
|
|
||||||
|
Please disclose responsibly by sending
|
||||||
|
a mail at security@workadventu.re (you can also ping us in the GitHub issues, but please, no details in the issues!)
|
||||||
|
|
||||||
|
We will assess the issue as soon as possible on a best-effort basis and will give you an estimate for when we have a fix
|
||||||
|
and release available for an eventual public disclosure.
|
||||||
|
|
||||||
|
We do not have a bug bounty program.
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We only apply security patches on the latest tagged release and on the `master` and `develop` branches
|
||||||
|
|
||||||
|
Unless specified otherwise, do not expect us to fix security issues on past releases. We are only maintaining one release:
|
||||||
|
the latest one, which is online at https://play.workadventu.re.
|
1
back/.prettierignore
Normal file
1
back/.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
src/Messages/generated
|
4
back/.prettierrc.json
Normal file
4
back/.prettierrc.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
@ -10,8 +10,11 @@
|
|||||||
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
"runprod": "node --max-old-space-size=4096 ./dist/server.js",
|
||||||
"profile": "tsc && node --prof ./dist/server.js",
|
"profile": "tsc && node --prof ./dist/server.js",
|
||||||
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
|
||||||
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
|
"lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts",
|
||||||
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts"
|
"fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts",
|
||||||
|
"precommit": "lint-staged",
|
||||||
|
"pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'",
|
||||||
|
"pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -37,24 +40,20 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
"homepage": "https://github.com/thecodingmachine/workadventure#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"@workadventure/tiled-map-type-guard": "^1.0.3",
|
||||||
"body-parser": "^1.19.0",
|
"axios": "^0.21.2",
|
||||||
"busboy": "^0.3.1",
|
"busboy": "^0.3.1",
|
||||||
"circular-json": "^0.5.9",
|
"circular-json": "^0.5.9",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
"generic-type-guard": "^3.2.0",
|
"generic-type-guard": "^3.2.0",
|
||||||
"google-protobuf": "^3.13.0",
|
"google-protobuf": "^3.13.0",
|
||||||
"grpc": "^1.24.4",
|
"grpc": "^1.24.4",
|
||||||
"http-status-codes": "^1.4.0",
|
"ipaddr.js": "^2.0.1",
|
||||||
"iterall": "^1.3.0",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"multer": "^1.4.2",
|
|
||||||
"prom-client": "^12.0.0",
|
"prom-client": "^12.0.0",
|
||||||
"query-string": "^6.13.3",
|
"query-string": "^6.13.3",
|
||||||
"systeminformation": "^4.31.1",
|
"redis": "^3.1.2",
|
||||||
"ts-node-dev": "^1.0.0-pre.44",
|
|
||||||
"typescript": "^3.8.3",
|
|
||||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||||
"uuidv4": "^6.0.7"
|
"uuidv4": "^6.0.7"
|
||||||
},
|
},
|
||||||
@ -67,10 +66,20 @@
|
|||||||
"@types/jasmine": "^3.5.10",
|
"@types/jasmine": "^3.5.10",
|
||||||
"@types/jsonwebtoken": "^8.3.8",
|
"@types/jsonwebtoken": "^8.3.8",
|
||||||
"@types/mkdirp": "^1.0.1",
|
"@types/mkdirp": "^1.0.1",
|
||||||
|
"@types/redis": "^2.8.31",
|
||||||
"@types/uuidv4": "^5.0.0",
|
"@types/uuidv4": "^5.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
"@typescript-eslint/eslint-plugin": "^2.26.0",
|
||||||
"@typescript-eslint/parser": "^2.26.0",
|
"@typescript-eslint/parser": "^2.26.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"jasmine": "^3.5.0"
|
"jasmine": "^3.5.0",
|
||||||
|
"lint-staged": "^11.0.0",
|
||||||
|
"prettier": "^2.3.1",
|
||||||
|
"ts-node-dev": "^1.0.0-pre.44",
|
||||||
|
"typescript": "^3.8.3"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.ts": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// lib/app.ts
|
// lib/app.ts
|
||||||
import {PrometheusController} from "./Controller/PrometheusController";
|
import { PrometheusController } from "./Controller/PrometheusController";
|
||||||
import {DebugController} from "./Controller/DebugController";
|
import { DebugController } from "./Controller/DebugController";
|
||||||
import {App as uwsApp} from "./Server/sifrr.server";
|
import { App as uwsApp } from "./Server/sifrr.server";
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public app: uwsApp;
|
public app: uwsApp;
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import {HttpResponse} from "uWebSockets.js";
|
import { HttpResponse } from "uWebSockets.js";
|
||||||
|
|
||||||
|
|
||||||
export class BaseController {
|
export class BaseController {
|
||||||
protected addCorsHeaders(res: HttpResponse): void {
|
protected addCorsHeaders(res: HttpResponse): void {
|
||||||
res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept");
|
||||||
res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
|
res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE");
|
||||||
res.writeHeader('access-control-allow-origin', '*');
|
res.writeHeader("access-control-allow-origin", "*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,53 +1,54 @@
|
|||||||
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable";
|
||||||
import {stringify} from "circular-json";
|
import { stringify } from "circular-json";
|
||||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||||
import { parse } from 'query-string';
|
import { parse } from "query-string";
|
||||||
import {App} from "../Server/sifrr.server";
|
import { App } from "../Server/sifrr.server";
|
||||||
import {socketManager} from "../Services/SocketManager";
|
import { socketManager } from "../Services/SocketManager";
|
||||||
|
|
||||||
export class DebugController {
|
export class DebugController {
|
||||||
constructor(private App : App) {
|
constructor(private App: App) {
|
||||||
this.getDump();
|
this.getDump();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDump() {
|
||||||
getDump(){
|
|
||||||
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
|
||||||
const query = parse(req.getQuery());
|
const query = parse(req.getQuery());
|
||||||
|
|
||||||
if (query.token !== ADMIN_API_TOKEN) {
|
if (query.token !== ADMIN_API_TOKEN) {
|
||||||
return res.status(401).send('Invalid token sent!');
|
return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify(
|
return res
|
||||||
socketManager.getWorlds(),
|
.writeStatus("200 OK")
|
||||||
(key: unknown, value: unknown) => {
|
.writeHeader("Content-Type", "application/json")
|
||||||
if (key === 'listeners') {
|
.end(
|
||||||
return 'Listeners';
|
stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => {
|
||||||
}
|
if (key === "listeners") {
|
||||||
if (key === 'socket') {
|
return "Listeners";
|
||||||
return 'Socket';
|
|
||||||
}
|
|
||||||
if (key === 'batchedMessages') {
|
|
||||||
return 'BatchedMessages';
|
|
||||||
}
|
|
||||||
if(value instanceof Map) {
|
|
||||||
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
for (const [mapKey, mapValue] of value.entries()) {
|
|
||||||
obj[mapKey] = mapValue;
|
|
||||||
}
|
}
|
||||||
return obj;
|
if (key === "socket") {
|
||||||
} else if(value instanceof Set) {
|
return "Socket";
|
||||||
|
}
|
||||||
|
if (key === "batchedMessages") {
|
||||||
|
return "BatchedMessages";
|
||||||
|
}
|
||||||
|
if (value instanceof Map) {
|
||||||
|
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
|
for (const [mapKey, mapValue] of value.entries()) {
|
||||||
|
obj[mapKey] = mapValue;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
} else if (value instanceof Set) {
|
||||||
const obj: Array<unknown> = [];
|
const obj: Array<unknown> = [];
|
||||||
for (const [setKey, setValue] of value.entries()) {
|
for (const [setKey, setValue] of value.entries()) {
|
||||||
obj.push(setValue);
|
obj.push(setValue);
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
} else {
|
} else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
));
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {App} from "../Server/sifrr.server";
|
import { App } from "../Server/sifrr.server";
|
||||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
import { HttpRequest, HttpResponse } from "uWebSockets.js";
|
||||||
const register = require('prom-client').register;
|
const register = require("prom-client").register;
|
||||||
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics;
|
const collectDefaultMetrics = require("prom-client").collectDefaultMetrics;
|
||||||
|
|
||||||
export class PrometheusController {
|
export class PrometheusController {
|
||||||
constructor(private App: App) {
|
constructor(private App: App) {
|
||||||
@ -14,7 +14,7 @@ export class PrometheusController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private metrics(res: HttpResponse, req: HttpRequest): void {
|
private metrics(res: HttpResponse, req: HttpRequest): void {
|
||||||
res.writeHeader('Content-Type', register.contentType);
|
res.writeHeader("Content-Type", register.contentType);
|
||||||
res.end(register.metrics());
|
res.end(register.metrics());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
||||||
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
||||||
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
|
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
|
||||||
const ADMIN_API_URL = process.env.ADMIN_API_URL || '';
|
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
|
||||||
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken';
|
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
|
||||||
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
||||||
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
|
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
|
||||||
const JITSI_ISS = process.env.JITSI_ISS || '';
|
const JITSI_ISS = process.env.JITSI_ISS || "";
|
||||||
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
|
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
|
||||||
const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080;
|
const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080;
|
||||||
const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051;
|
const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051;
|
||||||
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
|
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || "";
|
||||||
export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || '';
|
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
|
||||||
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4');
|
export const REDIS_HOST = process.env.REDIS_HOST || undefined;
|
||||||
|
export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379;
|
||||||
|
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
|
||||||
|
export const STORE_VARIABLES_FOR_LOCAL_MAPS = process.env.STORE_VARIABLES_FOR_LOCAL_MAPS === "true";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
@ -24,5 +27,5 @@ export {
|
|||||||
CPU_OVERHEAT_THRESHOLD,
|
CPU_OVERHEAT_THRESHOLD,
|
||||||
JITSI_URL,
|
JITSI_URL,
|
||||||
JITSI_ISS,
|
JITSI_ISS,
|
||||||
SECRET_JITSI_KEY
|
SECRET_JITSI_KEY,
|
||||||
}
|
};
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
BatchMessage,
|
|
||||||
PusherToBackMessage,
|
|
||||||
ServerToAdminClientMessage,
|
ServerToAdminClientMessage,
|
||||||
ServerToClientMessage,
|
UserJoinedRoomMessage,
|
||||||
SubMessage, UserJoinedRoomMessage, UserLeftRoomMessage
|
UserLeftRoomMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import {AdminSocket} from "../RoomManager";
|
import { AdminSocket } from "../RoomManager";
|
||||||
|
|
||||||
|
|
||||||
export class Admin {
|
export class Admin {
|
||||||
public constructor(
|
public constructor(private readonly socket: AdminSocket) {}
|
||||||
private readonly socket: AdminSocket
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendUserJoin(uuid: string, name: string, ip: string): void {
|
public sendUserJoin(uuid: string, name: string, ip: string): void {
|
||||||
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
||||||
@ -27,7 +21,7 @@ export class Admin {
|
|||||||
this.socket.write(serverToAdminClientMessage);
|
this.socket.write(serverToAdminClientMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendUserLeft(uuid: string/*, name: string, ip: string*/): void {
|
public sendUserLeft(uuid: string /*, name: string, ip: string*/): void {
|
||||||
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
const serverToAdminClientMessage = new ServerToAdminClientMessage();
|
||||||
|
|
||||||
const userLeftRoomMessage = new UserLeftRoomMessage();
|
const userLeftRoomMessage = new UserLeftRoomMessage();
|
||||||
|
@ -1,80 +1,94 @@
|
|||||||
import {PointInterface} from "./Websocket/PointInterface";
|
import { PointInterface } from "./Websocket/PointInterface";
|
||||||
import {Group} from "./Group";
|
import { Group } from "./Group";
|
||||||
import {User, UserSocket} from "./User";
|
import { User, UserSocket } from "./User";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone";
|
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone";
|
||||||
import {PositionNotifier} from "./PositionNotifier";
|
import { PositionNotifier } from "./PositionNotifier";
|
||||||
import {Movable} from "_Model/Movable";
|
import { Movable } from "_Model/Movable";
|
||||||
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
|
import {
|
||||||
import {arrayIntersect} from "../Services/ArrayHelper";
|
BatchToPusherMessage,
|
||||||
import {JoinRoomMessage} from "../Messages/generated/messages_pb";
|
BatchToPusherRoomMessage,
|
||||||
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
|
EmoteEventMessage,
|
||||||
import {ZoneSocket} from "src/RoomManager";
|
ErrorMessage,
|
||||||
import {Admin} from "../Model/Admin";
|
JoinRoomMessage,
|
||||||
|
SubToPusherRoomMessage,
|
||||||
|
VariableMessage,
|
||||||
|
VariableWithTagMessage,
|
||||||
|
} from "../Messages/generated/messages_pb";
|
||||||
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
|
import { RoomSocket, ZoneSocket } from "src/RoomManager";
|
||||||
|
import { Admin } from "../Model/Admin";
|
||||||
|
import { adminApi } from "../Services/AdminApi";
|
||||||
|
import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
|
||||||
|
import { ITiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||||
|
import { mapFetcher } from "../Services/MapFetcher";
|
||||||
|
import { VariablesManager } from "../Services/VariablesManager";
|
||||||
|
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
import { LocalUrlError } from "../Services/LocalUrlError";
|
||||||
|
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
|
||||||
|
import { VariableError } from "../Services/VariableError";
|
||||||
|
|
||||||
export type ConnectCallback = (user: User, group: Group) => void;
|
export type ConnectCallback = (user: User, group: Group) => void;
|
||||||
export type DisconnectCallback = (user: User, group: Group) => void;
|
export type DisconnectCallback = (user: User, group: Group) => void;
|
||||||
|
|
||||||
export enum GameRoomPolicyTypes {
|
|
||||||
ANONYMOUS_POLICY = 1,
|
|
||||||
MEMBERS_ONLY_POLICY,
|
|
||||||
USE_TAGS_POLICY,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GameRoom {
|
export class GameRoom {
|
||||||
private readonly minDistance: number;
|
|
||||||
private readonly groupRadius: number;
|
|
||||||
|
|
||||||
// Users, sorted by ID
|
// Users, sorted by ID
|
||||||
private readonly users: Map<number, User>;
|
private readonly users = new Map<number, User>();
|
||||||
private readonly usersByUuid: Map<string, User>;
|
private readonly usersByUuid = new Map<string, User>();
|
||||||
private readonly groups: Set<Group>;
|
private readonly groups = new Set<Group>();
|
||||||
private readonly admins: Set<Admin>;
|
private readonly admins = new Set<Admin>();
|
||||||
|
|
||||||
private readonly connectCallback: ConnectCallback;
|
private itemsState = new Map<number, unknown>();
|
||||||
private readonly disconnectCallback: DisconnectCallback;
|
|
||||||
|
|
||||||
private itemsState: Map<number, unknown> = new Map<number, unknown>();
|
|
||||||
|
|
||||||
private readonly positionNotifier: PositionNotifier;
|
private readonly positionNotifier: PositionNotifier;
|
||||||
public readonly roomId: string;
|
private versionNumber: number = 1;
|
||||||
public readonly roomSlug: string;
|
|
||||||
public readonly worldSlug: string = '';
|
|
||||||
public readonly organizationSlug: string = '';
|
|
||||||
private versionNumber:number = 1;
|
|
||||||
private nextUserId: number = 1;
|
private nextUserId: number = 1;
|
||||||
|
|
||||||
constructor(roomId: string,
|
private roomListeners: Set<RoomSocket> = new Set<RoomSocket>();
|
||||||
connectCallback: ConnectCallback,
|
|
||||||
disconnectCallback: DisconnectCallback,
|
|
||||||
minDistance: number,
|
|
||||||
groupRadius: number,
|
|
||||||
onEnters: EntersCallback,
|
|
||||||
onMoves: MovesCallback,
|
|
||||||
onLeaves: LeavesCallback)
|
|
||||||
{
|
|
||||||
this.roomId = roomId;
|
|
||||||
|
|
||||||
if (isRoomAnonymous(roomId)) {
|
private constructor(
|
||||||
this.roomSlug = extractRoomSlugPublicRoomId(this.roomId);
|
public readonly roomUrl: string,
|
||||||
} else {
|
private mapUrl: string,
|
||||||
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId);
|
private readonly connectCallback: ConnectCallback,
|
||||||
this.roomSlug = roomSlug;
|
private readonly disconnectCallback: DisconnectCallback,
|
||||||
this.organizationSlug = organizationSlug;
|
private readonly minDistance: number,
|
||||||
this.worldSlug = worldSlug;
|
private readonly groupRadius: number,
|
||||||
}
|
onEnters: EntersCallback,
|
||||||
|
onMoves: MovesCallback,
|
||||||
|
onLeaves: LeavesCallback,
|
||||||
this.users = new Map<number, User>();
|
onEmote: EmoteCallback
|
||||||
this.usersByUuid = new Map<string, User>();
|
) {
|
||||||
this.admins = new Set<Admin>();
|
|
||||||
this.groups = new Set<Group>();
|
|
||||||
this.connectCallback = connectCallback;
|
|
||||||
this.disconnectCallback = disconnectCallback;
|
|
||||||
this.minDistance = minDistance;
|
|
||||||
this.groupRadius = groupRadius;
|
|
||||||
// A zone is 10 sprites wide.
|
// A zone is 10 sprites wide.
|
||||||
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves);
|
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async create(
|
||||||
|
roomUrl: string,
|
||||||
|
connectCallback: ConnectCallback,
|
||||||
|
disconnectCallback: DisconnectCallback,
|
||||||
|
minDistance: number,
|
||||||
|
groupRadius: number,
|
||||||
|
onEnters: EntersCallback,
|
||||||
|
onMoves: MovesCallback,
|
||||||
|
onLeaves: LeavesCallback,
|
||||||
|
onEmote: EmoteCallback
|
||||||
|
): Promise<GameRoom> {
|
||||||
|
const mapDetails = await GameRoom.getMapDetails(roomUrl);
|
||||||
|
|
||||||
|
const gameRoom = new GameRoom(
|
||||||
|
roomUrl,
|
||||||
|
mapDetails.mapUrl,
|
||||||
|
connectCallback,
|
||||||
|
disconnectCallback,
|
||||||
|
minDistance,
|
||||||
|
groupRadius,
|
||||||
|
onEnters,
|
||||||
|
onMoves,
|
||||||
|
onLeaves,
|
||||||
|
onEmote
|
||||||
|
);
|
||||||
|
|
||||||
|
return gameRoom;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getGroups(): Group[] {
|
public getGroups(): Group[] {
|
||||||
@ -85,18 +99,31 @@ export class GameRoom {
|
|||||||
return this.users;
|
return this.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserByUuid(uuid: string): User|undefined {
|
public getUserByUuid(uuid: string): User | undefined {
|
||||||
return this.usersByUuid.get(uuid);
|
return this.usersByUuid.get(uuid);
|
||||||
}
|
}
|
||||||
|
public getUserById(id: number): User | undefined {
|
||||||
|
return this.users.get(id);
|
||||||
|
}
|
||||||
|
public getUsersByUuid(uuid: string): User[] {
|
||||||
|
const userList: User[] = [];
|
||||||
|
for (const user of this.users.values()) {
|
||||||
|
if (user.uuid === uuid) {
|
||||||
|
userList.push(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userList;
|
||||||
|
}
|
||||||
|
|
||||||
public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User {
|
public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User {
|
||||||
const positionMessage = joinRoomMessage.getPositionmessage();
|
const positionMessage = joinRoomMessage.getPositionmessage();
|
||||||
if (positionMessage === undefined) {
|
if (positionMessage === undefined) {
|
||||||
throw new Error('Missing position message');
|
throw new Error("Missing position message");
|
||||||
}
|
}
|
||||||
const position = ProtobufUtils.toPointInterface(positionMessage);
|
const position = ProtobufUtils.toPointInterface(positionMessage);
|
||||||
|
|
||||||
const user = new User(this.nextUserId,
|
const user = new User(
|
||||||
|
this.nextUserId,
|
||||||
joinRoomMessage.getUseruuid(),
|
joinRoomMessage.getUseruuid(),
|
||||||
joinRoomMessage.getIpaddress(),
|
joinRoomMessage.getIpaddress(),
|
||||||
position,
|
position,
|
||||||
@ -104,6 +131,7 @@ export class GameRoom {
|
|||||||
this.positionNotifier,
|
this.positionNotifier,
|
||||||
socket,
|
socket,
|
||||||
joinRoomMessage.getTagList(),
|
joinRoomMessage.getTagList(),
|
||||||
|
joinRoomMessage.getVisitcardurl(),
|
||||||
joinRoomMessage.getName(),
|
joinRoomMessage.getName(),
|
||||||
ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()),
|
ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()),
|
||||||
joinRoomMessage.getCompanion()
|
joinRoomMessage.getCompanion()
|
||||||
@ -121,12 +149,12 @@ export class GameRoom {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public leave(user : User){
|
public leave(user: User) {
|
||||||
const userObj = this.users.get(user.id);
|
const userObj = this.users.get(user.id);
|
||||||
if (userObj === undefined) {
|
if (userObj === undefined) {
|
||||||
console.warn('User ', user.id, 'does not belong to this game room! It should!');
|
console.warn("User ", user.id, "does not belong to this game room! It should!");
|
||||||
}
|
}
|
||||||
if (userObj !== undefined && typeof userObj.group !== 'undefined') {
|
if (userObj !== undefined && typeof userObj.group !== "undefined") {
|
||||||
this.leaveGroup(userObj);
|
this.leaveGroup(userObj);
|
||||||
}
|
}
|
||||||
this.users.delete(user.id);
|
this.users.delete(user.id);
|
||||||
@ -138,7 +166,7 @@ export class GameRoom {
|
|||||||
|
|
||||||
// Notify admins
|
// Notify admins
|
||||||
for (const admin of this.admins) {
|
for (const admin of this.admins) {
|
||||||
admin.sendUserLeft(user.uuid/*, user.name, user.IPAddress*/);
|
admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +174,7 @@ export class GameRoom {
|
|||||||
return this.users.size === 0 && this.admins.size === 0;
|
return this.users.size === 0 && this.admins.size === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePosition(user : User, userPosition: PointInterface): void {
|
public updatePosition(user: User, userPosition: PointInterface): void {
|
||||||
user.setPosition(userPosition);
|
user.setPosition(userPosition);
|
||||||
|
|
||||||
this.updateUserGroup(user);
|
this.updateUserGroup(user);
|
||||||
@ -154,6 +182,7 @@ export class GameRoom {
|
|||||||
|
|
||||||
private updateUserGroup(user: User): void {
|
private updateUserGroup(user: User): void {
|
||||||
user.group?.updatePosition();
|
user.group?.updatePosition();
|
||||||
|
user.group?.searchForNearbyUsers();
|
||||||
|
|
||||||
if (user.silent) {
|
if (user.silent) {
|
||||||
return;
|
return;
|
||||||
@ -168,22 +197,25 @@ export class GameRoom {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user);
|
const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user);
|
||||||
|
|
||||||
if (closestItem !== null) {
|
if (closestItem !== null) {
|
||||||
if (closestItem instanceof Group) {
|
if (closestItem instanceof Group) {
|
||||||
// Let's join the group!
|
// Let's join the group!
|
||||||
closestItem.join(user);
|
closestItem.join(user);
|
||||||
} else {
|
} else {
|
||||||
const closestUser : User = closestItem;
|
const closestUser: User = closestItem;
|
||||||
const group: Group = new Group(this.roomId,[
|
const group: Group = new Group(
|
||||||
user,
|
this.roomUrl,
|
||||||
closestUser
|
[user, closestUser],
|
||||||
], this.connectCallback, this.disconnectCallback, this.positionNotifier);
|
this.groupRadius,
|
||||||
|
this.connectCallback,
|
||||||
|
this.disconnectCallback,
|
||||||
|
this.positionNotifier
|
||||||
|
);
|
||||||
this.groups.add(group);
|
this.groups.add(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// If the user is part of a group:
|
// If the user is part of a group:
|
||||||
// should he leave the group?
|
// should he leave the group?
|
||||||
@ -224,7 +256,9 @@ export class GameRoom {
|
|||||||
this.positionNotifier.leave(group);
|
this.positionNotifier.leave(group);
|
||||||
group.destroy();
|
group.destroy();
|
||||||
if (!this.groups.has(group)) {
|
if (!this.groups.has(group)) {
|
||||||
throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World.");
|
throw new Error(
|
||||||
|
"Could not find group " + group.getId() + " referenced by user " + user.id + " in World."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.groups.delete(group);
|
this.groups.delete(group);
|
||||||
//todo: is the group garbage collected?
|
//todo: is the group garbage collected?
|
||||||
@ -242,16 +276,15 @@ export class GameRoom {
|
|||||||
* OR
|
* OR
|
||||||
* - close enough to a group (distance <= groupRadius)
|
* - close enough to a group (distance <= groupRadius)
|
||||||
*/
|
*/
|
||||||
private searchClosestAvailableUserOrGroup(user: User): User|Group|null
|
private searchClosestAvailableUserOrGroup(user: User): User | Group | null {
|
||||||
{
|
|
||||||
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
|
let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius);
|
||||||
let matchingItem: User | Group | null = null;
|
let matchingItem: User | Group | null = null;
|
||||||
this.users.forEach((currentUser, userId) => {
|
this.users.forEach((currentUser, userId) => {
|
||||||
// Let's only check users that are not part of a group
|
// Let's only check users that are not part of a group
|
||||||
if (typeof currentUser.group !== 'undefined') {
|
if (typeof currentUser.group !== "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(currentUser === user) {
|
if (currentUser === user) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentUser.silent) {
|
if (currentUser.silent) {
|
||||||
@ -260,7 +293,7 @@ export class GameRoom {
|
|||||||
|
|
||||||
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers.
|
||||||
|
|
||||||
if(distance <= minimumDistanceFound && distance <= this.minDistance) {
|
if (distance <= minimumDistanceFound && distance <= this.minDistance) {
|
||||||
minimumDistanceFound = distance;
|
minimumDistanceFound = distance;
|
||||||
matchingItem = currentUser;
|
matchingItem = currentUser;
|
||||||
}
|
}
|
||||||
@ -271,7 +304,7 @@ export class GameRoom {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition());
|
||||||
if(distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
if (distance <= minimumDistanceFound && distance <= this.groupRadius) {
|
||||||
minimumDistanceFound = distance;
|
minimumDistanceFound = distance;
|
||||||
matchingItem = group;
|
matchingItem = group;
|
||||||
}
|
}
|
||||||
@ -280,15 +313,15 @@ export class GameRoom {
|
|||||||
return matchingItem;
|
return matchingItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static computeDistance(user1: User, user2: User): number
|
public static computeDistance(user1: User, user2: User): number {
|
||||||
{
|
|
||||||
const user1Position = user1.getPosition();
|
const user1Position = user1.getPosition();
|
||||||
const user2Position = user2.getPosition();
|
const user2Position = user2.getPosition();
|
||||||
return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2));
|
return Math.sqrt(
|
||||||
|
Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number
|
public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number {
|
||||||
{
|
|
||||||
return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2));
|
return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,6 +333,69 @@ export class GameRoom {
|
|||||||
return this.itemsState;
|
return this.itemsState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async setVariable(name: string, value: string, user: User): Promise<void> {
|
||||||
|
// First, let's check if "user" is allowed to modify the variable.
|
||||||
|
const variableManager = await this.getVariableManager();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const readableBy = variableManager.setVariable(name, value, user);
|
||||||
|
|
||||||
|
// If the variable was not changed, let's not dispatch anything.
|
||||||
|
if (readableBy === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: should we batch those every 100ms?
|
||||||
|
const variableMessage = new VariableWithTagMessage();
|
||||||
|
variableMessage.setName(name);
|
||||||
|
variableMessage.setValue(value);
|
||||||
|
if (readableBy) {
|
||||||
|
variableMessage.setReadableby(readableBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subMessage = new SubToPusherRoomMessage();
|
||||||
|
subMessage.setVariablemessage(variableMessage);
|
||||||
|
|
||||||
|
const batchMessage = new BatchToPusherRoomMessage();
|
||||||
|
batchMessage.addPayload(subMessage);
|
||||||
|
|
||||||
|
// Dispatch the message on the room listeners
|
||||||
|
for (const socket of this.roomListeners) {
|
||||||
|
socket.write(batchMessage);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof VariableError) {
|
||||||
|
// Ok, we have an error setting a variable. Either the user is trying to hack the map... or the map
|
||||||
|
// is not up to date. So let's try to reload the map from scratch.
|
||||||
|
if (this.variableManagerLastLoad === undefined) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
const lastLoaded = new Date().getTime() - this.variableManagerLastLoad.getTime();
|
||||||
|
if (lastLoaded < 10000) {
|
||||||
|
console.log(
|
||||||
|
'An error occurred while setting the "' +
|
||||||
|
name +
|
||||||
|
"\" variable. But we tried to reload the map less than 10 seconds ago, so let's fail."
|
||||||
|
);
|
||||||
|
// Do not try to reload if we tried to reload less than 10 seconds ago.
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the variable manager
|
||||||
|
this.variableManagerPromise = undefined;
|
||||||
|
this.mapPromise = undefined;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'An error occurred while setting the "' + name + "\" variable. Let's reload the map and try again"
|
||||||
|
);
|
||||||
|
// Try to set the variable again!
|
||||||
|
await this.setVariable(name, value, user);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
|
||||||
return this.positionNotifier.addZoneListener(call, x, y);
|
return this.positionNotifier.addZoneListener(call, x, y);
|
||||||
}
|
}
|
||||||
@ -320,9 +416,124 @@ export class GameRoom {
|
|||||||
public adminLeave(admin: Admin): void {
|
public adminLeave(admin: Admin): void {
|
||||||
this.admins.delete(admin);
|
this.admins.delete(admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public incrementVersion(): number {
|
public incrementVersion(): number {
|
||||||
this.versionNumber++
|
this.versionNumber++;
|
||||||
return this.versionNumber;
|
return this.versionNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
|
||||||
|
this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public addRoomListener(socket: RoomSocket) {
|
||||||
|
this.roomListeners.add(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeRoomListener(socket: RoomSocket) {
|
||||||
|
this.roomListeners.delete(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects to the admin server to fetch map details.
|
||||||
|
* If there is no admin server, the map details are generated by analysing the map URL (that must be in the form: /_/instance/map_url)
|
||||||
|
*/
|
||||||
|
private static async getMapDetails(roomUrl: string): Promise<MapDetailsData> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
const roomUrlObj = new URL(roomUrl);
|
||||||
|
|
||||||
|
const match = /\/_\/[^/]+\/(.+)/.exec(roomUrlObj.pathname);
|
||||||
|
if (!match) {
|
||||||
|
console.error("Unexpected room URL", roomUrl);
|
||||||
|
throw new Error('Unexpected room URL "' + roomUrl + '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapUrl = roomUrlObj.protocol + "//" + match[1];
|
||||||
|
|
||||||
|
return {
|
||||||
|
mapUrl,
|
||||||
|
policy_type: 1,
|
||||||
|
textures: [],
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await adminApi.fetchMapDetails(roomUrl);
|
||||||
|
if (!isMapDetailsData(result)) {
|
||||||
|
console.error("Unexpected room details received from server", result);
|
||||||
|
throw new Error("Unexpected room details received from server");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapPromise: Promise<ITiledMap> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a promise to the map file.
|
||||||
|
* @throws LocalUrlError if the map we are trying to load is hosted on a local network
|
||||||
|
* @throws Error
|
||||||
|
*/
|
||||||
|
private getMap(): Promise<ITiledMap> {
|
||||||
|
if (!this.mapPromise) {
|
||||||
|
this.mapPromise = mapFetcher.fetchMap(this.mapUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private variableManagerPromise: Promise<VariablesManager> | undefined;
|
||||||
|
private variableManagerLastLoad: Date | undefined;
|
||||||
|
|
||||||
|
private getVariableManager(): Promise<VariablesManager> {
|
||||||
|
if (!this.variableManagerPromise) {
|
||||||
|
this.variableManagerLastLoad = new Date();
|
||||||
|
this.variableManagerPromise = this.getMap()
|
||||||
|
.then((map) => {
|
||||||
|
const variablesManager = new VariablesManager(this.roomUrl, map);
|
||||||
|
return variablesManager.init();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof LocalUrlError) {
|
||||||
|
// If we are trying to load a local URL, we are probably in test mode.
|
||||||
|
// In this case, let's bypass the server-side checks completely.
|
||||||
|
|
||||||
|
// Note: we run this message inside a setTimeout so that the room listeners can have time to connect.
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const roomListener of this.roomListeners) {
|
||||||
|
emitErrorOnRoomSocket(
|
||||||
|
roomListener,
|
||||||
|
"You are loading a local map. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const variablesManager = new VariablesManager(this.roomUrl, null);
|
||||||
|
return variablesManager.init();
|
||||||
|
} else {
|
||||||
|
// An error occurred while loading the map
|
||||||
|
// Right now, let's bypass the error. In the future, we should make sure the user is aware of that
|
||||||
|
// and that he/she will act on it to fix the problem.
|
||||||
|
|
||||||
|
// Note: we run this message inside a setTimeout so that the room listeners can have time to connect.
|
||||||
|
setTimeout(() => {
|
||||||
|
for (const roomListener of this.roomListeners) {
|
||||||
|
emitErrorOnRoomSocket(
|
||||||
|
roomListener,
|
||||||
|
"Your map does not seem accessible from the WorkAdventure servers. Is it behind a firewall or a proxy? Your map should be accessible from the WorkAdventure servers. If you use the scripting API in this map, please be aware that server-side checks and variable persistence is disabled."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
const variablesManager = new VariablesManager(this.roomUrl, null);
|
||||||
|
return variablesManager.init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.variableManagerPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getVariablesForTags(tags: string[]): Promise<Map<string, string>> {
|
||||||
|
const variablesManager = await this.getVariableManager();
|
||||||
|
return variablesManager.getVariablesForTags(tags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,34 @@
|
|||||||
import { ConnectCallback, DisconnectCallback } from "./GameRoom";
|
import { ConnectCallback, DisconnectCallback, GameRoom } from "./GameRoom";
|
||||||
import { User } from "./User";
|
import { User } from "./User";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
import {Movable} from "_Model/Movable";
|
import { Movable } from "_Model/Movable";
|
||||||
import {PositionNotifier} from "_Model/PositionNotifier";
|
import { PositionNotifier } from "_Model/PositionNotifier";
|
||||||
import {gaugeManager} from "../Services/GaugeManager";
|
import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
|
||||||
import {MAX_PER_GROUP} from "../Enum/EnvironmentVariable";
|
import type { Zone } from "../Model/Zone";
|
||||||
|
|
||||||
export class Group implements Movable {
|
export class Group implements Movable {
|
||||||
|
|
||||||
private static nextId: number = 1;
|
private static nextId: number = 1;
|
||||||
|
|
||||||
private id: number;
|
private id: number;
|
||||||
private users: Set<User>;
|
private users: Set<User>;
|
||||||
private x!: number;
|
private x!: number;
|
||||||
private y!: number;
|
private y!: number;
|
||||||
private hasEditedGauge: boolean = false;
|
|
||||||
private wasDestroyed: boolean = false;
|
private wasDestroyed: boolean = false;
|
||||||
private roomId: string;
|
private roomId: string;
|
||||||
|
private currentZone: Zone | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) {
|
roomId: string,
|
||||||
|
users: User[],
|
||||||
|
private groupRadius: number,
|
||||||
|
private connectCallback: ConnectCallback,
|
||||||
|
private disconnectCallback: DisconnectCallback,
|
||||||
|
private positionNotifier: PositionNotifier
|
||||||
|
) {
|
||||||
this.roomId = roomId;
|
this.roomId = roomId;
|
||||||
this.users = new Set<User>();
|
this.users = new Set<User>();
|
||||||
this.id = Group.nextId;
|
this.id = Group.nextId;
|
||||||
Group.nextId++;
|
Group.nextId++;
|
||||||
//we only send a event for prometheus metrics if the group lives more than 5 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!this.wasDestroyed) {
|
|
||||||
this.hasEditedGauge = true;
|
|
||||||
gaugeManager.incNbGroupsPerRoomGauge(roomId);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
users.forEach((user: User) => {
|
users.forEach((user: User) => {
|
||||||
this.join(user);
|
this.join(user);
|
||||||
@ -43,7 +41,7 @@ export class Group implements Movable {
|
|||||||
return Array.from(this.users.values());
|
return Array.from(this.users.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
getId() : number {
|
getId(): number {
|
||||||
return this.id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +51,7 @@ export class Group implements Movable {
|
|||||||
getPosition(): PositionInterface {
|
getPosition(): PositionInterface {
|
||||||
return {
|
return {
|
||||||
x: this.x,
|
x: this.x,
|
||||||
y: this.y
|
y: this.y,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,9 +79,22 @@ export class Group implements Movable {
|
|||||||
this.y = y;
|
this.y = y;
|
||||||
|
|
||||||
if (oldX === undefined) {
|
if (oldX === undefined) {
|
||||||
this.positionNotifier.enter(this);
|
this.currentZone = this.positionNotifier.enter(this);
|
||||||
} else {
|
} else {
|
||||||
this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY});
|
this.currentZone = this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchForNearbyUsers(): void {
|
||||||
|
if (!this.currentZone) return;
|
||||||
|
|
||||||
|
for (const user of this.positionNotifier.getAllUsersInSquareAroundZone(this.currentZone)) {
|
||||||
|
if (user.group || this.isFull()) return; //we ignore users that are already in a group.
|
||||||
|
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), this.getPosition());
|
||||||
|
if (distance < this.groupRadius) {
|
||||||
|
this.join(user);
|
||||||
|
this.updatePosition();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,19 +106,17 @@ export class Group implements Movable {
|
|||||||
return this.users.size <= 1;
|
return this.users.size <= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
join(user: User): void
|
join(user: User): void {
|
||||||
{
|
|
||||||
// Broadcast on the right event
|
// Broadcast on the right event
|
||||||
this.connectCallback(user, this);
|
this.connectCallback(user, this);
|
||||||
this.users.add(user);
|
this.users.add(user);
|
||||||
user.group = this;
|
user.group = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
leave(user: User): void
|
leave(user: User): void {
|
||||||
{
|
|
||||||
const success = this.users.delete(user);
|
const success = this.users.delete(user);
|
||||||
if (success === false) {
|
if (success === false) {
|
||||||
throw new Error("Could not find user "+user.id+" in the group "+this.id);
|
throw new Error("Could not find user " + user.id + " in the group " + this.id);
|
||||||
}
|
}
|
||||||
user.group = undefined;
|
user.group = undefined;
|
||||||
|
|
||||||
@ -123,16 +132,14 @@ export class Group implements Movable {
|
|||||||
* Let's kick everybody out.
|
* Let's kick everybody out.
|
||||||
* Usually used when there is only one user left.
|
* Usually used when there is only one user left.
|
||||||
*/
|
*/
|
||||||
destroy(): void
|
destroy(): void {
|
||||||
{
|
|
||||||
if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId);
|
|
||||||
for (const user of this.users) {
|
for (const user of this.users) {
|
||||||
this.leave(user);
|
this.leave(user);
|
||||||
}
|
}
|
||||||
this.wasDestroyed = true;
|
this.wasDestroyed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
get getSize(){
|
get getSize() {
|
||||||
return this.users.size;
|
return this.users.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A physical object that can be placed into a Zone
|
* A physical object that can be placed into a Zone
|
||||||
*/
|
*/
|
||||||
export interface Movable {
|
export interface Movable {
|
||||||
getPosition(): PositionInterface
|
getPosition(): PositionInterface;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export interface PositionInterface {
|
export interface PositionInterface {
|
||||||
x: number,
|
x: number;
|
||||||
y: number
|
y: number;
|
||||||
}
|
}
|
||||||
|
@ -8,40 +8,59 @@
|
|||||||
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
|
||||||
* number of players around the current player.
|
* number of players around the current player.
|
||||||
*/
|
*/
|
||||||
import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
|
import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone";
|
||||||
import {Movable} from "_Model/Movable";
|
import { Movable } from "_Model/Movable";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
import {ZoneSocket} from "../RoomManager";
|
import { ZoneSocket } from "../RoomManager";
|
||||||
|
import { User } from "../Model/User";
|
||||||
|
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
|
||||||
|
|
||||||
interface ZoneDescriptor {
|
interface ZoneDescriptor {
|
||||||
i: number;
|
i: number;
|
||||||
j: number;
|
j: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PositionNotifier {
|
export function* getNearbyDescriptorsMatrix(middleZoneDescriptor: ZoneDescriptor): Generator<ZoneDescriptor> {
|
||||||
|
for (let n = 0; n < 9; n++) {
|
||||||
|
const i = middleZoneDescriptor.i + ((n % 3) - 1);
|
||||||
|
const j = middleZoneDescriptor.j + (Math.floor(n / 3) - 1);
|
||||||
|
|
||||||
// TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!)
|
if (i >= 0 && j >= 0) {
|
||||||
|
yield { i, j };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PositionNotifier {
|
||||||
|
// TODO: we need a way to clean the zones if no one is in the zone and no one listening (to free memory!)
|
||||||
|
|
||||||
private zones: Zone[][] = [];
|
private zones: Zone[][] = [];
|
||||||
|
|
||||||
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback) {
|
constructor(
|
||||||
}
|
private zoneWidth: number,
|
||||||
|
private zoneHeight: number,
|
||||||
|
private onUserEnters: EntersCallback,
|
||||||
|
private onUserMoves: MovesCallback,
|
||||||
|
private onUserLeaves: LeavesCallback,
|
||||||
|
private onEmote: EmoteCallback
|
||||||
|
) {}
|
||||||
|
|
||||||
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
|
||||||
return {
|
return {
|
||||||
i: Math.floor(x / this.zoneWidth),
|
i: Math.floor(x / this.zoneWidth),
|
||||||
j: Math.floor(y / this.zoneHeight),
|
j: Math.floor(y / this.zoneHeight),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public enter(thing: Movable): void {
|
public enter(thing: Movable): Zone {
|
||||||
const position = thing.getPosition();
|
const position = thing.getPosition();
|
||||||
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
const zoneDesc = this.getZoneDescriptorFromCoordinates(position.x, position.y);
|
||||||
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
||||||
zone.enter(thing, null, position);
|
zone.enter(thing, null, position);
|
||||||
|
return zone;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): void {
|
public updatePosition(thing: Movable, newPosition: PositionInterface, oldPosition: PositionInterface): Zone {
|
||||||
// Did we change zone?
|
// Did we change zone?
|
||||||
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
const oldZoneDesc = this.getZoneDescriptorFromCoordinates(oldPosition.x, oldPosition.y);
|
||||||
const newZoneDesc = this.getZoneDescriptorFromCoordinates(newPosition.x, newPosition.y);
|
const newZoneDesc = this.getZoneDescriptorFromCoordinates(newPosition.x, newPosition.y);
|
||||||
@ -55,9 +74,11 @@ export class PositionNotifier {
|
|||||||
|
|
||||||
// Enter new zone
|
// Enter new zone
|
||||||
newZone.enter(thing, oldZone, newPosition);
|
newZone.enter(thing, oldZone, newPosition);
|
||||||
|
return newZone;
|
||||||
} else {
|
} else {
|
||||||
const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
const zone = this.getZone(oldZoneDesc.i, oldZoneDesc.j);
|
||||||
zone.move(thing, newPosition);
|
zone.move(thing, newPosition);
|
||||||
|
return zone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +98,7 @@ export class PositionNotifier {
|
|||||||
|
|
||||||
let zone = this.zones[j][i];
|
let zone = this.zones[j][i];
|
||||||
if (zone === undefined) {
|
if (zone === undefined) {
|
||||||
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, i, j);
|
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, this.onEmote, i, j);
|
||||||
this.zones[j][i] = zone;
|
this.zones[j][i] = zone;
|
||||||
}
|
}
|
||||||
return zone;
|
return zone;
|
||||||
@ -93,4 +114,22 @@ export class PositionNotifier {
|
|||||||
const zone = this.getZone(x, y);
|
const zone = this.getZone(x, y);
|
||||||
zone.removeListener(call);
|
zone.removeListener(call);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
|
||||||
|
const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y);
|
||||||
|
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
|
||||||
|
zone.emitEmoteEvent(emoteEventMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public *getAllUsersInSquareAroundZone(zone: Zone): Generator<User> {
|
||||||
|
const zoneDescriptor = this.getZoneDescriptorFromCoordinates(zone.x, zone.y);
|
||||||
|
for (const d of getNearbyDescriptorsMatrix(zoneDescriptor)) {
|
||||||
|
const zone = this.getZone(d.i, d.j);
|
||||||
|
for (const thing of zone.getThings()) {
|
||||||
|
if (thing instanceof User) {
|
||||||
|
yield thing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
//helper functions to parse room IDs
|
|
||||||
|
|
||||||
export const isRoomAnonymous = (roomID: string): boolean => {
|
|
||||||
if (roomID.startsWith('_/')) {
|
|
||||||
return true;
|
|
||||||
} else if(roomID.startsWith('@/')) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
throw new Error('Incorrect room ID: '+roomID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const extractRoomSlugPublicRoomId = (roomId: string): string => {
|
|
||||||
const idParts = roomId.split('/');
|
|
||||||
if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId);
|
|
||||||
return idParts.slice(2).join('/');
|
|
||||||
}
|
|
||||||
export interface extractDataFromPrivateRoomIdResponse {
|
|
||||||
organizationSlug: string;
|
|
||||||
worldSlug: string;
|
|
||||||
roomSlug: string;
|
|
||||||
}
|
|
||||||
export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => {
|
|
||||||
const idParts = roomId.split('/');
|
|
||||||
if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId);
|
|
||||||
const organizationSlug = idParts[1];
|
|
||||||
const worldSlug = idParts[2];
|
|
||||||
const roomSlug = idParts[3];
|
|
||||||
return {organizationSlug, worldSlug, roomSlug}
|
|
||||||
}
|
|
@ -1,11 +1,17 @@
|
|||||||
import { Group } from "./Group";
|
import { Group } from "./Group";
|
||||||
import { PointInterface } from "./Websocket/PointInterface";
|
import { PointInterface } from "./Websocket/PointInterface";
|
||||||
import {Zone} from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
import {Movable} from "_Model/Movable";
|
import { Movable } from "_Model/Movable";
|
||||||
import {PositionNotifier} from "_Model/PositionNotifier";
|
import { PositionNotifier } from "_Model/PositionNotifier";
|
||||||
import {ServerDuplexStream} from "grpc";
|
import { ServerDuplexStream } from "grpc";
|
||||||
import {BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb";
|
import {
|
||||||
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
|
BatchMessage,
|
||||||
|
CompanionMessage,
|
||||||
|
PusherToBackMessage,
|
||||||
|
ServerToClientMessage,
|
||||||
|
SubMessage,
|
||||||
|
} from "../Messages/generated/messages_pb";
|
||||||
|
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
||||||
|
|
||||||
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
export type UserSocket = ServerDuplexStream<PusherToBackMessage, ServerToClientMessage>;
|
||||||
|
|
||||||
@ -22,6 +28,7 @@ export class User implements Movable {
|
|||||||
private positionNotifier: PositionNotifier,
|
private positionNotifier: PositionNotifier,
|
||||||
public readonly socket: UserSocket,
|
public readonly socket: UserSocket,
|
||||||
public readonly tags: string[],
|
public readonly tags: string[],
|
||||||
|
public readonly visitCardUrl: string | null,
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly characterLayers: CharacterLayer[],
|
public readonly characterLayers: CharacterLayer[],
|
||||||
public readonly companion?: CompanionMessage
|
public readonly companion?: CompanionMessage
|
||||||
@ -41,9 +48,8 @@ export class User implements Movable {
|
|||||||
this.positionNotifier.updatePosition(this, position, oldPosition);
|
this.positionNotifier.updatePosition(this, position, oldPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private batchedMessages: BatchMessage = new BatchMessage();
|
private batchedMessages: BatchMessage = new BatchMessage();
|
||||||
private batchTimeout: NodeJS.Timeout|null = null;
|
private batchTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
public emitInBatch(payload: SubMessage): void {
|
public emitInBatch(payload: SubMessage): void {
|
||||||
this.batchedMessages.addPayload(payload);
|
this.batchedMessages.addPayload(payload);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export interface CharacterLayer {
|
export interface CharacterLayer {
|
||||||
name: string,
|
name: string;
|
||||||
url: string|undefined
|
url: string | undefined;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import * as tg from "generic-type-guard";
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
export const isItemEventMessageInterface =
|
export const isItemEventMessageInterface = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
itemId: tg.isNumber,
|
itemId: tg.isNumber,
|
||||||
event: tg.isString,
|
event: tg.isString,
|
||||||
state: tg.isUnknown,
|
state: tg.isUnknown,
|
||||||
parameters: tg.isUnknown,
|
parameters: tg.isUnknown,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
export type ItemEventMessageInterface = tg.GuardedType<typeof isItemEventMessageInterface>;
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import {PointInterface} from "./PointInterface";
|
import { PointInterface } from "./PointInterface";
|
||||||
|
|
||||||
export class Point implements PointInterface{
|
export class Point implements PointInterface {
|
||||||
constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) {
|
constructor(
|
||||||
}
|
public x: number,
|
||||||
|
public y: number,
|
||||||
|
public direction: string = "none",
|
||||||
|
public moving: boolean = false
|
||||||
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,11 +7,12 @@ import * as tg from "generic-type-guard";
|
|||||||
readonly moving: boolean;
|
readonly moving: boolean;
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
export const isPointInterface =
|
export const isPointInterface = new tg.IsInterface()
|
||||||
new tg.IsInterface().withProperties({
|
.withProperties({
|
||||||
x: tg.isNumber,
|
x: tg.isNumber,
|
||||||
y: tg.isNumber,
|
y: tg.isNumber,
|
||||||
direction: tg.isString,
|
direction: tg.isString,
|
||||||
moving: tg.isBoolean
|
moving: tg.isBoolean,
|
||||||
}).get();
|
})
|
||||||
|
.get();
|
||||||
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
export type PointInterface = tg.GuardedType<typeof isPointInterface>;
|
||||||
|
@ -1,34 +1,33 @@
|
|||||||
import {PointInterface} from "./PointInterface";
|
import { PointInterface } from "./PointInterface";
|
||||||
import {
|
import {
|
||||||
CharacterLayerMessage,
|
CharacterLayerMessage,
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
PointMessage,
|
PointMessage,
|
||||||
PositionMessage
|
PositionMessage,
|
||||||
} from "../../Messages/generated/messages_pb";
|
} from "../../Messages/generated/messages_pb";
|
||||||
import {CharacterLayer} from "_Model/Websocket/CharacterLayer";
|
import { CharacterLayer } from "_Model/Websocket/CharacterLayer";
|
||||||
import Direction = PositionMessage.Direction;
|
import Direction = PositionMessage.Direction;
|
||||||
import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage";
|
import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
|
|
||||||
export class ProtobufUtils {
|
export class ProtobufUtils {
|
||||||
|
|
||||||
public static toPositionMessage(point: PointInterface): PositionMessage {
|
public static toPositionMessage(point: PointInterface): PositionMessage {
|
||||||
let direction: Direction;
|
let direction: Direction;
|
||||||
switch (point.direction) {
|
switch (point.direction) {
|
||||||
case 'up':
|
case "up":
|
||||||
direction = Direction.UP;
|
direction = Direction.UP;
|
||||||
break;
|
break;
|
||||||
case 'down':
|
case "down":
|
||||||
direction = Direction.DOWN;
|
direction = Direction.DOWN;
|
||||||
break;
|
break;
|
||||||
case 'left':
|
case "left":
|
||||||
direction = Direction.LEFT;
|
direction = Direction.LEFT;
|
||||||
break;
|
break;
|
||||||
case 'right':
|
case "right":
|
||||||
direction = Direction.RIGHT;
|
direction = Direction.RIGHT;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('unexpected direction');
|
throw new Error("unexpected direction");
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = new PositionMessage();
|
const position = new PositionMessage();
|
||||||
@ -44,16 +43,16 @@ export class ProtobufUtils {
|
|||||||
let direction: string;
|
let direction: string;
|
||||||
switch (position.getDirection()) {
|
switch (position.getDirection()) {
|
||||||
case Direction.UP:
|
case Direction.UP:
|
||||||
direction = 'up';
|
direction = "up";
|
||||||
break;
|
break;
|
||||||
case Direction.DOWN:
|
case Direction.DOWN:
|
||||||
direction = 'down';
|
direction = "down";
|
||||||
break;
|
break;
|
||||||
case Direction.LEFT:
|
case Direction.LEFT:
|
||||||
direction = 'left';
|
direction = "left";
|
||||||
break;
|
break;
|
||||||
case Direction.RIGHT:
|
case Direction.RIGHT:
|
||||||
direction = 'right';
|
direction = "right";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Unexpected direction");
|
throw new Error("Unexpected direction");
|
||||||
@ -82,7 +81,7 @@ export class ProtobufUtils {
|
|||||||
event: itemEventMessage.getEvent(),
|
event: itemEventMessage.getEvent(),
|
||||||
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
parameters: JSON.parse(itemEventMessage.getParametersjson()),
|
||||||
state: JSON.parse(itemEventMessage.getStatejson()),
|
state: JSON.parse(itemEventMessage.getStatejson()),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage {
|
||||||
@ -96,7 +95,7 @@ export class ProtobufUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] {
|
||||||
return characterLayers.map(function(characterLayer): CharacterLayerMessage {
|
return characterLayers.map(function (characterLayer): CharacterLayerMessage {
|
||||||
const message = new CharacterLayerMessage();
|
const message = new CharacterLayerMessage();
|
||||||
message.setName(characterLayer.name);
|
message.setName(characterLayer.name);
|
||||||
if (characterLayer.url) {
|
if (characterLayer.url) {
|
||||||
@ -107,7 +106,7 @@ export class ProtobufUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
|
public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] {
|
||||||
return characterLayers.map(function(characterLayer): CharacterLayer {
|
return characterLayers.map(function (characterLayer): CharacterLayer {
|
||||||
const url = characterLayer.getUrl();
|
const url = characterLayer.getUrl();
|
||||||
return {
|
return {
|
||||||
name: characterLayer.getName(),
|
name: characterLayer.getName(),
|
||||||
|
@ -1,37 +1,52 @@
|
|||||||
import {User} from "./User";
|
import { User } from "./User";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import { PositionInterface } from "_Model/PositionInterface";
|
||||||
import {Movable} from "./Movable";
|
import { Movable } from "./Movable";
|
||||||
import {Group} from "./Group";
|
import { Group } from "./Group";
|
||||||
import {ZoneSocket} from "../RoomManager";
|
import { ZoneSocket } from "../RoomManager";
|
||||||
|
import { EmoteEventMessage } from "../Messages/generated/messages_pb";
|
||||||
|
|
||||||
export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void;
|
export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void;
|
||||||
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
|
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
|
||||||
export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void;
|
export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void;
|
||||||
|
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
|
||||||
|
|
||||||
export class Zone {
|
export class Zone {
|
||||||
private things: Set<Movable> = new Set<Movable>();
|
private things: Set<Movable> = new Set<Movable>();
|
||||||
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
|
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
|
||||||
|
|
||||||
/**
|
constructor(
|
||||||
* @param x For debugging purpose only
|
private onEnters: EntersCallback,
|
||||||
* @param y For debugging purpose only
|
private onMoves: MovesCallback,
|
||||||
*/
|
private onLeaves: LeavesCallback,
|
||||||
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, public readonly x: number, public readonly y: number) {
|
private onEmote: EmoteCallback,
|
||||||
}
|
public readonly x: number,
|
||||||
|
public readonly y: number
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A user/thing leaves the zone
|
* A user/thing leaves the zone
|
||||||
*/
|
*/
|
||||||
public leave(thing: Movable, newZone: Zone|null) {
|
public leave(thing: Movable, newZone: Zone | null) {
|
||||||
const result = this.things.delete(thing);
|
const result = this.things.delete(thing);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
throw new Error('Could not find user in zone '+thing.id);
|
throw new Error("Could not find user in zone " + thing.id);
|
||||||
}
|
}
|
||||||
if (thing instanceof Group) {
|
if (thing instanceof Group) {
|
||||||
throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')');
|
throw new Error(
|
||||||
|
"Could not find group " +
|
||||||
|
thing.getId() +
|
||||||
|
" in zone (" +
|
||||||
|
this.x +
|
||||||
|
"," +
|
||||||
|
this.y +
|
||||||
|
"). Position of group: (" +
|
||||||
|
thing.getPosition().x +
|
||||||
|
"," +
|
||||||
|
thing.getPosition().y +
|
||||||
|
")"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
this.notifyLeft(thing, newZone);
|
this.notifyLeft(thing, newZone);
|
||||||
}
|
}
|
||||||
@ -39,15 +54,13 @@ export class Zone {
|
|||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this user/thing left
|
* Notify listeners of this zone that this user/thing left
|
||||||
*/
|
*/
|
||||||
private notifyLeft(thing: Movable, newZone: Zone|null) {
|
private notifyLeft(thing: Movable, newZone: Zone | null) {
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
//if (listener !== thing && (newZone === null || !listener.listenedZones.has(newZone))) {
|
this.onLeaves(thing, newZone, listener);
|
||||||
this.onLeaves(thing, newZone, listener);
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
|
public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
||||||
this.things.add(thing);
|
this.things.add(thing);
|
||||||
this.notifyEnter(thing, oldZone, position);
|
this.notifyEnter(thing, oldZone, position);
|
||||||
}
|
}
|
||||||
@ -55,22 +68,12 @@ export class Zone {
|
|||||||
/**
|
/**
|
||||||
* Notify listeners of this zone that this user entered
|
* Notify listeners of this zone that this user entered
|
||||||
*/
|
*/
|
||||||
private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
|
private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
|
|
||||||
/*if (listener === thing) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (oldZone === null || !listener.listenedZones.has(oldZone)) {
|
|
||||||
this.onEnters(thing, listener);
|
|
||||||
} else {
|
|
||||||
this.onMoves(thing, position, listener);
|
|
||||||
}*/
|
|
||||||
this.onEnters(thing, oldZone, listener);
|
this.onEnters(thing, oldZone, listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public move(thing: Movable, position: PositionInterface) {
|
public move(thing: Movable, position: PositionInterface) {
|
||||||
if (!this.things.has(thing)) {
|
if (!this.things.has(thing)) {
|
||||||
this.things.add(thing);
|
this.things.add(thing);
|
||||||
@ -80,33 +83,11 @@ export class Zone {
|
|||||||
|
|
||||||
for (const listener of this.listeners) {
|
for (const listener of this.listeners) {
|
||||||
//if (listener !== thing) {
|
//if (listener !== thing) {
|
||||||
this.onMoves(thing,position, listener);
|
this.onMoves(thing, position, listener);
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*public startListening(listener: User): void {
|
|
||||||
for (const thing of this.things) {
|
|
||||||
if (thing !== listener) {
|
|
||||||
this.onEnters(thing, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listeners.add(listener);
|
|
||||||
listener.listenedZones.add(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public stopListening(listener: User): void {
|
|
||||||
for (const thing of this.things) {
|
|
||||||
if (thing !== listener) {
|
|
||||||
this.onLeaves(thing, listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listeners.delete(listener);
|
|
||||||
listener.listenedZones.delete(this);
|
|
||||||
}*/
|
|
||||||
|
|
||||||
public getThings(): Set<Movable> {
|
public getThings(): Set<Movable> {
|
||||||
return this.things;
|
return this.things;
|
||||||
}
|
}
|
||||||
@ -119,4 +100,10 @@ export class Zone {
|
|||||||
public removeListener(socket: ZoneSocket): void {
|
public removeListener(socket: ZoneSocket): void {
|
||||||
this.listeners.delete(socket);
|
this.listeners.delete(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public emitEmoteEvent(emoteEventMessage: EmoteEventMessage) {
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
this.onEmote(emoteEventMessage, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,99 +1,145 @@
|
|||||||
import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb";
|
import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb";
|
||||||
import {
|
import {
|
||||||
AdminGlobalMessage,
|
AdminGlobalMessage,
|
||||||
AdminMessage,
|
AdminMessage,
|
||||||
AdminPusherToBackMessage,
|
AdminPusherToBackMessage,
|
||||||
AdminRoomMessage,
|
AdminRoomMessage,
|
||||||
BanMessage,
|
BanMessage,
|
||||||
|
BatchToPusherMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
|
EmotePromptMessage,
|
||||||
EmptyMessage,
|
EmptyMessage,
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
JoinRoomMessage,
|
JoinRoomMessage,
|
||||||
PlayGlobalMessage,
|
PlayGlobalMessage,
|
||||||
PusherToBackMessage,
|
PusherToBackMessage,
|
||||||
QueryJitsiJwtMessage, RefreshRoomPromptMessage,
|
QueryJitsiJwtMessage,
|
||||||
|
RefreshRoomPromptMessage,
|
||||||
|
RoomMessage,
|
||||||
ServerToAdminClientMessage,
|
ServerToAdminClientMessage,
|
||||||
ServerToClientMessage,
|
|
||||||
SilentMessage,
|
SilentMessage,
|
||||||
UserMovesMessage,
|
UserMovesMessage,
|
||||||
WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage,
|
VariableMessage,
|
||||||
ZoneMessage
|
WebRtcSignalToServerMessage,
|
||||||
|
WorldFullWarningToRoomMessage,
|
||||||
|
ZoneMessage,
|
||||||
} from "./Messages/generated/messages_pb";
|
} from "./Messages/generated/messages_pb";
|
||||||
import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc";
|
import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc";
|
||||||
import {socketManager} from "./Services/SocketManager";
|
import { socketManager } from "./Services/SocketManager";
|
||||||
import {emitError} from "./Services/MessageHelpers";
|
import { emitError, emitErrorOnRoomSocket, emitErrorOnZoneSocket } from "./Services/MessageHelpers";
|
||||||
import {User, UserSocket} from "./Model/User";
|
import { User, UserSocket } from "./Model/User";
|
||||||
import {GameRoom} from "./Model/GameRoom";
|
import { GameRoom } from "./Model/GameRoom";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
import {Admin} from "./Model/Admin";
|
import { Admin } from "./Model/Admin";
|
||||||
|
|
||||||
const debug = Debug('roommanager');
|
const debug = Debug("roommanager");
|
||||||
|
|
||||||
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
export type AdminSocket = ServerDuplexStream<AdminPusherToBackMessage, ServerToAdminClientMessage>;
|
||||||
export type ZoneSocket = ServerWritableStream<ZoneMessage, ServerToClientMessage>;
|
export type ZoneSocket = ServerWritableStream<ZoneMessage, BatchToPusherMessage>;
|
||||||
|
export type RoomSocket = ServerWritableStream<RoomMessage, BatchToPusherRoomMessage>;
|
||||||
|
|
||||||
const roomManager: IRoomManagerServer = {
|
const roomManager: IRoomManagerServer = {
|
||||||
joinRoom: (call: UserSocket): void => {
|
joinRoom: (call: UserSocket): void => {
|
||||||
console.log('joinRoom called');
|
console.log("joinRoom called");
|
||||||
|
|
||||||
let room: GameRoom|null = null;
|
let room: GameRoom | null = null;
|
||||||
let user: User|null = null;
|
let user: User | null = null;
|
||||||
|
|
||||||
call.on('data', (message: PusherToBackMessage) => {
|
call.on("data", (message: PusherToBackMessage) => {
|
||||||
try {
|
(async () => {
|
||||||
if (room === null || user === null) {
|
try {
|
||||||
if (message.hasJoinroommessage()) {
|
if (room === null || user === null) {
|
||||||
socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => {
|
if (message.hasJoinroommessage()) {
|
||||||
if (call.writable) {
|
socketManager
|
||||||
room = gameRoom;
|
.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage)
|
||||||
user = myUser;
|
.then(({ room: gameRoom, user: myUser }) => {
|
||||||
} else {
|
if (call.writable) {
|
||||||
//Connexion may have been closed before the init was finished, so we have to manually disconnect the user.
|
room = gameRoom;
|
||||||
socketManager.leaveRoom(gameRoom, myUser);
|
user = myUser;
|
||||||
|
} else {
|
||||||
|
//Connection may have been closed before the init was finished, so we have to manually disconnect the user.
|
||||||
|
socketManager.leaveRoom(gameRoom, myUser);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => emitError(call, e));
|
||||||
|
} else {
|
||||||
|
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (message.hasJoinroommessage()) {
|
||||||
|
throw new Error("Cannot call JoinRoomMessage twice!");
|
||||||
|
} else if (message.hasUsermovesmessage()) {
|
||||||
|
socketManager.handleUserMovesMessage(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getUsermovesmessage() as UserMovesMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasSilentmessage()) {
|
||||||
|
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
||||||
|
} else if (message.hasItemeventmessage()) {
|
||||||
|
socketManager.handleItemEvent(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getItemeventmessage() as ItemEventMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasVariablemessage()) {
|
||||||
|
await socketManager.handleVariableEvent(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getVariablemessage() as VariableMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasWebrtcsignaltoservermessage()) {
|
||||||
|
socketManager.emitVideo(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
||||||
|
socketManager.emitScreenSharing(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasPlayglobalmessage()) {
|
||||||
|
socketManager.emitPlayGlobalMessage(
|
||||||
|
room,
|
||||||
|
message.getPlayglobalmessage() as PlayGlobalMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasQueryjitsijwtmessage()) {
|
||||||
|
socketManager.handleQueryJitsiJwtMessage(
|
||||||
|
user,
|
||||||
|
message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasEmotepromptmessage()) {
|
||||||
|
socketManager.handleEmoteEventMessage(
|
||||||
|
room,
|
||||||
|
user,
|
||||||
|
message.getEmotepromptmessage() as EmotePromptMessage
|
||||||
|
);
|
||||||
|
} else if (message.hasSendusermessage()) {
|
||||||
|
const sendUserMessage = message.getSendusermessage();
|
||||||
|
if (sendUserMessage !== undefined) {
|
||||||
|
socketManager.handlerSendUserMessage(user, sendUserMessage);
|
||||||
}
|
}
|
||||||
});
|
} else if (message.hasBanusermessage()) {
|
||||||
} else {
|
const banUserMessage = message.getBanusermessage();
|
||||||
throw new Error('The first message sent MUST be of type JoinRoomMessage');
|
if (banUserMessage !== undefined) {
|
||||||
}
|
socketManager.handlerBanUserMessage(room, user, banUserMessage);
|
||||||
} else {
|
}
|
||||||
if (message.hasJoinroommessage()) {
|
} else {
|
||||||
throw new Error('Cannot call JoinRoomMessage twice!');
|
throw new Error("Unhandled message type");
|
||||||
} else if (message.hasUsermovesmessage()) {
|
|
||||||
socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage);
|
|
||||||
} else if (message.hasSilentmessage()) {
|
|
||||||
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
|
|
||||||
} else if (message.hasItemeventmessage()) {
|
|
||||||
socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage);
|
|
||||||
} else if (message.hasWebrtcsignaltoservermessage()) {
|
|
||||||
socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage);
|
|
||||||
} else if (message.hasWebrtcscreensharingsignaltoservermessage()) {
|
|
||||||
socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage);
|
|
||||||
} else if (message.hasPlayglobalmessage()) {
|
|
||||||
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
|
|
||||||
} else if (message.hasQueryjitsijwtmessage()){
|
|
||||||
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
|
|
||||||
}else if (message.hasSendusermessage()) {
|
|
||||||
const sendUserMessage = message.getSendusermessage();
|
|
||||||
if(sendUserMessage !== undefined) {
|
|
||||||
socketManager.handlerSendUserMessage(user, sendUserMessage);
|
|
||||||
}
|
}
|
||||||
}else if (message.hasBanusermessage()) {
|
|
||||||
const banUserMessage = message.getBanusermessage();
|
|
||||||
if(banUserMessage !== undefined) {
|
|
||||||
socketManager.handlerBanUserMessage(room, user, banUserMessage);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Unhandled message type');
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
emitError(call, e);
|
||||||
|
call.end();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
})().catch((e) => console.error(e));
|
||||||
emitError(call, e);
|
|
||||||
call.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on('end', () => {
|
call.on("end", () => {
|
||||||
debug('joinRoom ended');
|
debug("joinRoom ended");
|
||||||
if (user !== null && room !== null) {
|
if (user !== null && room !== null) {
|
||||||
socketManager.leaveRoom(room, user);
|
socketManager.leaveRoom(room, user);
|
||||||
}
|
}
|
||||||
@ -102,61 +148,96 @@ const roomManager: IRoomManagerServer = {
|
|||||||
user = null;
|
user = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on('error', (err: Error) => {
|
call.on("error", (err: Error) => {
|
||||||
console.error('An error occurred in joinRoom stream:', err);
|
console.error("An error occurred in joinRoom stream:", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
listenZone(call: ZoneSocket): void {
|
listenZone(call: ZoneSocket): void {
|
||||||
debug('listenZone called');
|
debug("listenZone called");
|
||||||
const zoneMessage = call.request;
|
const zoneMessage = call.request;
|
||||||
|
|
||||||
socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager
|
||||||
|
.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => {
|
||||||
|
emitErrorOnZoneSocket(call, e.toString());
|
||||||
|
});
|
||||||
|
|
||||||
call.on('cancelled', () => {
|
call.on("cancelled", () => {
|
||||||
debug('listenZone cancelled');
|
debug("listenZone cancelled");
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager
|
||||||
|
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
})
|
});
|
||||||
|
|
||||||
call.on('close', () => {
|
call.on("close", () => {
|
||||||
debug('listenZone connection closed');
|
debug("listenZone connection closed");
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
socketManager
|
||||||
}).on('error', (e) => {
|
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
console.error('An error occurred in listenZone stream:', e);
|
.catch((e) => console.error(e));
|
||||||
socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY());
|
}).on("error", (e) => {
|
||||||
|
console.error("An error occurred in listenZone stream:", e);
|
||||||
|
socketManager
|
||||||
|
.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
call.end();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
listenRoom(call: RoomSocket): void {
|
||||||
|
debug("listenRoom called");
|
||||||
|
const roomMessage = call.request;
|
||||||
|
|
||||||
|
socketManager.addRoomListener(call, roomMessage.getRoomid()).catch((e) => {
|
||||||
|
emitErrorOnRoomSocket(call, e.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
call.on("cancelled", () => {
|
||||||
|
debug("listenRoom cancelled");
|
||||||
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||||
|
call.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
call.on("close", () => {
|
||||||
|
debug("listenRoom connection closed");
|
||||||
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||||
|
}).on("error", (e) => {
|
||||||
|
console.error("An error occurred in listenRoom stream:", e);
|
||||||
|
socketManager.removeRoomListener(call, roomMessage.getRoomid()).catch((e) => console.error(e));
|
||||||
call.end();
|
call.end();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
adminRoom(call: AdminSocket): void {
|
adminRoom(call: AdminSocket): void {
|
||||||
console.log('adminRoom called');
|
console.log("adminRoom called");
|
||||||
|
|
||||||
const admin = new Admin(call);
|
const admin = new Admin(call);
|
||||||
let room: GameRoom|null = null;
|
let room: GameRoom | null = null;
|
||||||
|
|
||||||
call.on('data', (message: AdminPusherToBackMessage) => {
|
call.on("data", (message: AdminPusherToBackMessage) => {
|
||||||
try {
|
try {
|
||||||
if (room === null) {
|
if (room === null) {
|
||||||
if (message.hasSubscribetoroom()) {
|
if (message.hasSubscribetoroom()) {
|
||||||
const roomId = message.getSubscribetoroom();
|
const roomId = message.getSubscribetoroom();
|
||||||
socketManager.handleJoinAdminRoom(admin, roomId).then((gameRoom: GameRoom) => {
|
socketManager
|
||||||
room = gameRoom;
|
.handleJoinAdminRoom(admin, roomId)
|
||||||
});
|
.then((gameRoom: GameRoom) => {
|
||||||
|
room = gameRoom;
|
||||||
|
})
|
||||||
|
.catch((e) => console.error(e));
|
||||||
} else {
|
} else {
|
||||||
throw new Error('The first message sent MUST be of type JoinRoomMessage');
|
throw new Error("The first message sent MUST be of type JoinRoomMessage");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emitError(call, e);
|
emitError(call, e);
|
||||||
call.end();
|
call.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on('end', () => {
|
call.on("end", () => {
|
||||||
debug('joinRoom ended');
|
debug("joinRoom ended");
|
||||||
if (room !== null) {
|
if (room !== null) {
|
||||||
socketManager.leaveAdminRoom(room, admin);
|
socketManager.leaveAdminRoom(room, admin);
|
||||||
}
|
}
|
||||||
@ -164,39 +245,53 @@ const roomManager: IRoomManagerServer = {
|
|||||||
room = null;
|
room = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
call.on('error', (err: Error) => {
|
call.on("error", (err: Error) => {
|
||||||
console.error('An error occurred in joinAdminRoom stream:', err);
|
console.error("An error occurred in joinAdminRoom stream:", err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendAdminMessage(call: ServerUnaryCall<AdminMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
|
socketManager
|
||||||
socketManager.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage());
|
.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendGlobalAdminMessage(call: ServerUnaryCall<AdminGlobalMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
throw new Error('Not implemented yet');
|
throw new Error("Not implemented yet");
|
||||||
// TODO
|
// TODO
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
ban(call: ServerUnaryCall<BanMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
// FIXME Work in progress
|
// FIXME Work in progress
|
||||||
socketManager.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage());
|
socketManager
|
||||||
|
.banUser(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
|
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendAdminMessageToRoom(call: ServerUnaryCall<AdminRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
||||||
socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage());
|
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||||
|
socketManager
|
||||||
|
.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage(), call.request.getType())
|
||||||
|
.catch((e) => console.error(e));
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendWorldFullWarningToRoom(call: ServerUnaryCall<WorldFullWarningToRoomMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendWorldFullWarningToRoom(
|
||||||
socketManager.dispatchWorlFullWarning(call.request.getRoomid());
|
call: ServerUnaryCall<WorldFullWarningToRoomMessage>,
|
||||||
|
callback: sendUnaryData<EmptyMessage>
|
||||||
|
): void {
|
||||||
|
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||||
|
socketManager.dispatchWorldFullWarning(call.request.getRoomid()).catch((e) => console.error(e));
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
sendRefreshRoomPrompt(call: ServerUnaryCall<RefreshRoomPromptMessage>, callback: sendUnaryData<EmptyMessage>): void {
|
sendRefreshRoomPrompt(
|
||||||
socketManager.dispatchRoomRefresh(call.request.getRoomid());
|
call: ServerUnaryCall<RefreshRoomPromptMessage>,
|
||||||
|
callback: sendUnaryData<EmptyMessage>
|
||||||
|
): void {
|
||||||
|
// FIXME: we could improve return message by returning a Success|ErrorMessage message
|
||||||
|
socketManager.dispatchRoomRefresh(call.request.getRoomid()).catch((e) => console.error(e));
|
||||||
callback(null, new EmptyMessage());
|
callback(null, new EmptyMessage());
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export {roomManager};
|
export { roomManager };
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { App as _App, AppOptions } from 'uWebSockets.js';
|
import { App as _App, AppOptions } from "uWebSockets.js";
|
||||||
import BaseApp from './baseapp';
|
import BaseApp from "./baseapp";
|
||||||
import { extend } from './utils';
|
import { extend } from "./utils";
|
||||||
import { UwsApp } from './types';
|
import { UwsApp } from "./types";
|
||||||
|
|
||||||
class App extends (<UwsApp>_App) {
|
class App extends (<UwsApp>_App) {
|
||||||
constructor(options: AppOptions = {}) {
|
constructor(options: AppOptions = {}) {
|
||||||
super(options); // eslint-disable-line constructor-super
|
super(options); // eslint-disable-line constructor-super
|
||||||
extend(this, new BaseApp());
|
extend(this, new BaseApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
@ -1,116 +1,109 @@
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from "stream";
|
||||||
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||||
|
|
||||||
import formData from './formdata';
|
import formData from "./formdata";
|
||||||
import { stob } from './utils';
|
import { stob } from "./utils";
|
||||||
import { Handler } from './types';
|
import { Handler } from "./types";
|
||||||
import {join} from "path";
|
import { join } from "path";
|
||||||
|
|
||||||
const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
|
const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
|
||||||
const noOp = () => true;
|
const noOp = () => true;
|
||||||
|
|
||||||
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
const handleBody = (res: HttpResponse, req: HttpRequest) => {
|
||||||
const contType = req.getHeader('content-type');
|
const contType = req.getHeader("content-type");
|
||||||
|
|
||||||
res.bodyStream = function() {
|
res.bodyStream = function () {
|
||||||
const stream = new Readable();
|
const stream = new Readable();
|
||||||
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method
|
||||||
|
|
||||||
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
this.onData((ab: ArrayBuffer, isLast: boolean) => {
|
||||||
// uint and then slicing is bit faster than slice and then uint
|
// uint and then slicing is bit faster than slice and then uint
|
||||||
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
if (isLast) {
|
if (isLast) {
|
||||||
stream.push(null);
|
stream.push(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return stream;
|
return stream;
|
||||||
};
|
};
|
||||||
|
|
||||||
res.body = () => stob(res.bodyStream());
|
res.body = () => stob(res.bodyStream());
|
||||||
|
|
||||||
if (contType.includes('application/json'))
|
if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body());
|
||||||
res.json = async () => JSON.parse(await res.body());
|
if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType);
|
||||||
if (contTypes.map(t => contType.includes(t)).includes(true))
|
|
||||||
res.formData = formData.bind(res, contType);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class BaseApp {
|
class BaseApp {
|
||||||
_sockets = new Map();
|
_sockets = new Map();
|
||||||
ws!: TemplatedApp['ws'];
|
ws!: TemplatedApp["ws"];
|
||||||
get!: TemplatedApp['get'];
|
get!: TemplatedApp["get"];
|
||||||
_post!: TemplatedApp['post'];
|
_post!: TemplatedApp["post"];
|
||||||
_put!: TemplatedApp['put'];
|
_put!: TemplatedApp["put"];
|
||||||
_patch!: TemplatedApp['patch'];
|
_patch!: TemplatedApp["patch"];
|
||||||
_listen!: TemplatedApp['listen'];
|
_listen!: TemplatedApp["listen"];
|
||||||
|
|
||||||
post(pattern: string, handler: Handler) {
|
post(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== 'function')
|
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
this._post(pattern, (res, req) => {
|
||||||
this._post(pattern, (res, req) => {
|
handleBody(res, req);
|
||||||
handleBody(res, req);
|
handler(res, req);
|
||||||
handler(res, req);
|
});
|
||||||
});
|
return this;
|
||||||
return this;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
put(pattern: string, handler: Handler) {
|
put(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== 'function')
|
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
this._put(pattern, (res, req) => {
|
||||||
this._put(pattern, (res, req) => {
|
handleBody(res, req);
|
||||||
handleBody(res, req);
|
|
||||||
|
|
||||||
handler(res, req);
|
handler(res, req);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
patch(pattern: string, handler: Handler) {
|
patch(pattern: string, handler: Handler) {
|
||||||
if (typeof handler !== 'function')
|
if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`);
|
||||||
throw Error(`handler should be a function, given ${typeof handler}.`);
|
this._patch(pattern, (res, req) => {
|
||||||
this._patch(pattern, (res, req) => {
|
handleBody(res, req);
|
||||||
handleBody(res, req);
|
|
||||||
|
|
||||||
handler(res, req);
|
handler(res, req);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
listen(h: string | number, p: Function | number = noOp, cb?: Function) {
|
||||||
if (typeof p === 'number' && typeof h === 'string') {
|
if (typeof p === "number" && typeof h === "string") {
|
||||||
this._listen(h, p, socket => {
|
this._listen(h, p, (socket) => {
|
||||||
this._sockets.set(p, socket);
|
this._sockets.set(p, socket);
|
||||||
if (cb === undefined) {
|
if (cb === undefined) {
|
||||||
throw new Error('cb undefined');
|
throw new Error("cb undefined");
|
||||||
|
}
|
||||||
|
cb(socket);
|
||||||
|
});
|
||||||
|
} else if (typeof h === "number" && typeof p === "function") {
|
||||||
|
this._listen(h, (socket) => {
|
||||||
|
this._sockets.set(h, socket);
|
||||||
|
p(socket);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)");
|
||||||
}
|
}
|
||||||
cb(socket);
|
|
||||||
});
|
return this;
|
||||||
} else if (typeof h === 'number' && typeof p === 'function') {
|
|
||||||
this._listen(h, socket => {
|
|
||||||
this._sockets.set(h, socket);
|
|
||||||
p(socket);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw Error(
|
|
||||||
'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
close(port: null | number = null) {
|
||||||
}
|
if (port) {
|
||||||
|
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
||||||
close(port: null | number = null) {
|
this._sockets.delete(port);
|
||||||
if (port) {
|
} else {
|
||||||
this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port));
|
this._sockets.forEach((app) => {
|
||||||
this._sockets.delete(port);
|
us_listen_socket_close(app);
|
||||||
} else {
|
});
|
||||||
this._sockets.forEach(app => {
|
this._sockets.clear();
|
||||||
us_listen_socket_close(app);
|
}
|
||||||
});
|
return this;
|
||||||
this._sockets.clear();
|
|
||||||
}
|
}
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BaseApp;
|
export default BaseApp;
|
||||||
|
@ -1,100 +1,99 @@
|
|||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from "fs";
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from "path";
|
||||||
import Busboy from 'busboy';
|
import Busboy from "busboy";
|
||||||
import mkdirp from 'mkdirp';
|
import mkdirp from "mkdirp";
|
||||||
|
|
||||||
function formData(
|
function formData(
|
||||||
contType: string,
|
contType: string,
|
||||||
options: busboy.BusboyConfig & {
|
options: busboy.BusboyConfig & {
|
||||||
abortOnLimit?: boolean;
|
abortOnLimit?: boolean;
|
||||||
tmpDir?: string;
|
tmpDir?: string;
|
||||||
onFile?: (
|
onFile?: (
|
||||||
fieldname: string,
|
fieldname: string,
|
||||||
file: NodeJS.ReadableStream,
|
file: NodeJS.ReadableStream,
|
||||||
filename: string,
|
filename: string,
|
||||||
encoding: string,
|
encoding: string,
|
||||||
mimetype: string
|
mimetype: string
|
||||||
) => string;
|
) => string;
|
||||||
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
filename?: (oldName: string) => string;
|
filename?: (oldName: string) => string;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
console.log('Enter form data');
|
console.log("Enter form data");
|
||||||
options.headers = {
|
options.headers = {
|
||||||
'content-type': contType
|
"content-type": contType,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const busb = new Busboy(options);
|
const busb = new Busboy(options);
|
||||||
const ret = {};
|
const ret = {};
|
||||||
|
|
||||||
this.bodyStream().pipe(busb);
|
this.bodyStream().pipe(busb);
|
||||||
|
|
||||||
busb.on('limit', () => {
|
busb.on("limit", () => {
|
||||||
if (options.abortOnLimit) {
|
if (options.abortOnLimit) {
|
||||||
reject(Error('limit'));
|
reject(Error("limit"));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("file", function (fieldname, file, filename, encoding, mimetype) {
|
||||||
|
const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = {
|
||||||
|
filename,
|
||||||
|
encoding,
|
||||||
|
mimetype,
|
||||||
|
filePath: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof options.tmpDir === "string") {
|
||||||
|
if (typeof options.filename === "function") filename = options.filename(filename);
|
||||||
|
const fileToSave = join(options.tmpDir, filename);
|
||||||
|
mkdirp(dirname(fileToSave));
|
||||||
|
|
||||||
|
file.pipe(createWriteStream(fileToSave));
|
||||||
|
value.filePath = fileToSave;
|
||||||
|
}
|
||||||
|
if (typeof options.onFile === "function") {
|
||||||
|
value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRetValue(ret, fieldname, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("field", function (fieldname, value) {
|
||||||
|
if (typeof options.onField === "function") options.onField(fieldname, value);
|
||||||
|
|
||||||
|
setRetValue(ret, fieldname, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("finish", function () {
|
||||||
|
resolve(ret);
|
||||||
|
});
|
||||||
|
|
||||||
|
busb.on("error", reject);
|
||||||
});
|
});
|
||||||
|
|
||||||
busb.on('file', function(fieldname, file, filename, encoding, mimetype) {
|
|
||||||
const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = {
|
|
||||||
filename,
|
|
||||||
encoding,
|
|
||||||
mimetype,
|
|
||||||
filePath: undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof options.tmpDir === 'string') {
|
|
||||||
if (typeof options.filename === 'function') filename = options.filename(filename);
|
|
||||||
const fileToSave = join(options.tmpDir, filename);
|
|
||||||
mkdirp(dirname(fileToSave));
|
|
||||||
|
|
||||||
file.pipe(createWriteStream(fileToSave));
|
|
||||||
value.filePath = fileToSave;
|
|
||||||
}
|
|
||||||
if (typeof options.onFile === 'function') {
|
|
||||||
value.filePath =
|
|
||||||
options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRetValue(ret, fieldname, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on('field', function(fieldname, value) {
|
|
||||||
if (typeof options.onField === 'function') options.onField(fieldname, value);
|
|
||||||
|
|
||||||
setRetValue(ret, fieldname, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on('finish', function() {
|
|
||||||
resolve(ret);
|
|
||||||
});
|
|
||||||
|
|
||||||
busb.on('error', reject);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setRetValue(
|
function setRetValue(
|
||||||
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
fieldname: string,
|
fieldname: string,
|
||||||
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
) {
|
) {
|
||||||
if (fieldname.endsWith('[]')) {
|
if (fieldname.endsWith("[]")) {
|
||||||
fieldname = fieldname.slice(0, fieldname.length - 2);
|
fieldname = fieldname.slice(0, fieldname.length - 2);
|
||||||
if (Array.isArray(ret[fieldname])) {
|
if (Array.isArray(ret[fieldname])) {
|
||||||
ret[fieldname].push(value);
|
ret[fieldname].push(value);
|
||||||
|
} else {
|
||||||
|
ret[fieldname] = [value];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ret[fieldname] = [value];
|
if (Array.isArray(ret[fieldname])) {
|
||||||
|
ret[fieldname].push(value);
|
||||||
|
} else if (ret[fieldname]) {
|
||||||
|
ret[fieldname] = [ret[fieldname], value];
|
||||||
|
} else {
|
||||||
|
ret[fieldname] = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
if (Array.isArray(ret[fieldname])) {
|
|
||||||
ret[fieldname].push(value);
|
|
||||||
} else if (ret[fieldname]) {
|
|
||||||
ret[fieldname] = [ret[fieldname], value];
|
|
||||||
} else {
|
|
||||||
ret[fieldname] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default formData;
|
export default formData;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js';
|
import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js";
|
||||||
import BaseApp from './baseapp';
|
import BaseApp from "./baseapp";
|
||||||
import { extend } from './utils';
|
import { extend } from "./utils";
|
||||||
import { UwsApp } from './types';
|
import { UwsApp } from "./types";
|
||||||
|
|
||||||
class SSLApp extends (<UwsApp>_SSLApp) {
|
class SSLApp extends (<UwsApp>_SSLApp) {
|
||||||
constructor(options: AppOptions) {
|
constructor(options: AppOptions) {
|
||||||
super(options); // eslint-disable-line constructor-super
|
super(options); // eslint-disable-line constructor-super
|
||||||
extend(this, new BaseApp());
|
extend(this, new BaseApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SSLApp;
|
export default SSLApp;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js';
|
import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js";
|
||||||
|
|
||||||
export type UwsApp = {
|
export type UwsApp = {
|
||||||
(options: AppOptions): TemplatedApp;
|
(options: AppOptions): TemplatedApp;
|
||||||
new (options: AppOptions): TemplatedApp;
|
new (options: AppOptions): TemplatedApp;
|
||||||
prototype: TemplatedApp;
|
prototype: TemplatedApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
export type Handler = (res: HttpResponse, req: HttpRequest) => void;
|
||||||
|
@ -1,37 +1,36 @@
|
|||||||
import { ReadStream } from 'fs';
|
import { ReadStream } from "fs";
|
||||||
|
|
||||||
function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(
|
function extend(who: any, from: any, overwrite = true) {
|
||||||
Object.keys(from)
|
const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from));
|
||||||
);
|
ownProps.forEach((prop) => {
|
||||||
ownProps.forEach(prop => {
|
if (prop === "constructor" || from[prop] === undefined) return;
|
||||||
if (prop === 'constructor' || from[prop] === undefined) return;
|
if (who[prop] && overwrite) {
|
||||||
if (who[prop] && overwrite) {
|
who[`_${prop}`] = who[prop];
|
||||||
who[`_${prop}`] = who[prop];
|
}
|
||||||
}
|
if (typeof from[prop] === "function") who[prop] = from[prop].bind(who);
|
||||||
if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who);
|
else who[prop] = from[prop];
|
||||||
else who[prop] = from[prop];
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stob(stream: ReadStream): Promise<Buffer> {
|
function stob(stream: ReadStream): Promise<Buffer> {
|
||||||
return new Promise(resolve => {
|
return new Promise((resolve) => {
|
||||||
const buffers: Buffer[] = [];
|
const buffers: Buffer[] = [];
|
||||||
stream.on('data', buffers.push.bind(buffers));
|
stream.on("data", buffers.push.bind(buffers));
|
||||||
|
|
||||||
stream.on('end', () => {
|
stream.on("end", () => {
|
||||||
switch (buffers.length) {
|
switch (buffers.length) {
|
||||||
case 0:
|
case 0:
|
||||||
resolve(Buffer.allocUnsafe(0));
|
resolve(Buffer.allocUnsafe(0));
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
resolve(buffers[0]);
|
resolve(buffers[0]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
resolve(Buffer.concat(buffers));
|
resolve(Buffer.concat(buffers));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { extend, stob };
|
export { extend, stob };
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { parse } from 'query-string';
|
import { parse } from "query-string";
|
||||||
import { HttpRequest } from 'uWebSockets.js';
|
import { HttpRequest } from "uWebSockets.js";
|
||||||
import App from './server/app';
|
import App from "./server/app";
|
||||||
import SSLApp from './server/sslapp';
|
import SSLApp from "./server/sslapp";
|
||||||
import * as types from './server/types';
|
import * as types from "./server/types";
|
||||||
|
|
||||||
const getQuery = (req: HttpRequest) => {
|
const getQuery = (req: HttpRequest) => {
|
||||||
return parse(req.getQuery());
|
return parse(req.getQuery());
|
||||||
};
|
};
|
||||||
|
|
||||||
export { App, SSLApp, getQuery };
|
export { App, SSLApp, getQuery };
|
||||||
export * from './server/types';
|
export * from "./server/types";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
App,
|
App,
|
||||||
SSLApp,
|
SSLApp,
|
||||||
getQuery,
|
getQuery,
|
||||||
...types
|
...types,
|
||||||
};
|
};
|
||||||
|
24
back/src/Services/AdminApi.ts
Normal file
24
back/src/Services/AdminApi.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
import Axios from "axios";
|
||||||
|
import { MapDetailsData } from "./AdminApi/MapDetailsData";
|
||||||
|
import { RoomRedirect } from "./AdminApi/RoomRedirect";
|
||||||
|
|
||||||
|
class AdminApi {
|
||||||
|
async fetchMapDetails(playUri: string): Promise<MapDetailsData | RoomRedirect> {
|
||||||
|
if (!ADMIN_API_URL) {
|
||||||
|
return Promise.reject(new Error("No admin backoffice set!"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: { playUri: string } = {
|
||||||
|
playUri,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await Axios.get(ADMIN_API_URL + "/api/map", {
|
||||||
|
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const adminApi = new AdminApi();
|
11
back/src/Services/AdminApi/CharacterTexture.ts
Normal file
11
back/src/Services/AdminApi/CharacterTexture.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isCharacterTexture = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
id: tg.isNumber,
|
||||||
|
level: tg.isNumber,
|
||||||
|
url: tg.isString,
|
||||||
|
rights: tg.isString,
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
export type CharacterTexture = tg.GuardedType<typeof isCharacterTexture>;
|
21
back/src/Services/AdminApi/MapDetailsData.ts
Normal file
21
back/src/Services/AdminApi/MapDetailsData.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
import { isCharacterTexture } from "./CharacterTexture";
|
||||||
|
import { isAny, isNumber } from "generic-type-guard";
|
||||||
|
|
||||||
|
/*const isNumericEnum =
|
||||||
|
<T extends { [n: number]: string }>(vs: T) =>
|
||||||
|
(v: any): v is T =>
|
||||||
|
typeof v === "number" && v in vs;*/
|
||||||
|
|
||||||
|
export const isMapDetailsData = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
mapUrl: tg.isString,
|
||||||
|
policy_type: isNumber, //isNumericEnum(GameRoomPolicyTypes),
|
||||||
|
tags: tg.isArray(tg.isString),
|
||||||
|
textures: tg.isArray(isCharacterTexture),
|
||||||
|
})
|
||||||
|
.withOptionalProperties({
|
||||||
|
roomSlug: tg.isUnion(tg.isString, tg.isNull), // deprecated
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
export type MapDetailsData = tg.GuardedType<typeof isMapDetailsData>;
|
8
back/src/Services/AdminApi/RoomRedirect.ts
Normal file
8
back/src/Services/AdminApi/RoomRedirect.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import * as tg from "generic-type-guard";
|
||||||
|
|
||||||
|
export const isRoomRedirect = new tg.IsInterface()
|
||||||
|
.withProperties({
|
||||||
|
redirectUrl: tg.isString,
|
||||||
|
})
|
||||||
|
.get();
|
||||||
|
export type RoomRedirect = tg.GuardedType<typeof isRoomRedirect>;
|
@ -1,3 +1,3 @@
|
|||||||
export const arrayIntersect = (array1: string[], array2: string[]) : boolean => {
|
export const arrayIntersect = (array1: string[], array2: string[]): boolean => {
|
||||||
return array1.filter(value => array2.includes(value)).length > 0;
|
return array1.filter((value) => array2.includes(value)).length > 0;
|
||||||
}
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
const EventEmitter = require('events');
|
const EventEmitter = require("events");
|
||||||
|
|
||||||
const clientJoinEvent = 'clientJoin';
|
const clientJoinEvent = "clientJoin";
|
||||||
const clientLeaveEvent = 'clientLeave';
|
const clientLeaveEvent = "clientLeave";
|
||||||
|
|
||||||
class ClientEventsEmitter extends EventEmitter {
|
class ClientEventsEmitter extends EventEmitter {
|
||||||
emitClientJoin(clientUUid: string, roomId: string): void {
|
emitClientJoin(clientUUid: string, roomId: string): void {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable";
|
import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
function secNSec2ms(secNSec: Array<number>|number) {
|
function secNSec2ms(secNSec: Array<number> | number) {
|
||||||
if (Array.isArray(secNSec)) {
|
if (Array.isArray(secNSec)) {
|
||||||
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
return secNSec[0] * 1000 + secNSec[1] / 1000000;
|
||||||
}
|
}
|
||||||
@ -12,17 +12,17 @@ class CpuTracker {
|
|||||||
private overHeating: boolean = false;
|
private overHeating: boolean = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let time = process.hrtime.bigint()
|
let time = process.hrtime.bigint();
|
||||||
let usage = process.cpuUsage()
|
let usage = process.cpuUsage();
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const elapTime = process.hrtime.bigint();
|
const elapTime = process.hrtime.bigint();
|
||||||
const elapUsage = process.cpuUsage(usage)
|
const elapUsage = process.cpuUsage(usage);
|
||||||
usage = process.cpuUsage()
|
usage = process.cpuUsage();
|
||||||
|
|
||||||
const elapTimeMS = elapTime - time;
|
const elapTimeMS = elapTime - time;
|
||||||
const elapUserMS = secNSec2ms(elapUsage.user)
|
const elapUserMS = secNSec2ms(elapUsage.user);
|
||||||
const elapSystMS = secNSec2ms(elapUsage.system)
|
const elapSystMS = secNSec2ms(elapUsage.system);
|
||||||
this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000)
|
this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000);
|
||||||
|
|
||||||
time = elapTime;
|
time = elapTime;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Counter, Gauge} from "prom-client";
|
import { Counter, Gauge } from "prom-client";
|
||||||
|
|
||||||
//this class should manage all the custom metrics used by prometheus
|
//this class should manage all the custom metrics used by prometheus
|
||||||
class GaugeManager {
|
class GaugeManager {
|
||||||
@ -10,29 +10,29 @@ class GaugeManager {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.nbRoomsGauge = new Gauge({
|
this.nbRoomsGauge = new Gauge({
|
||||||
name: 'workadventure_nb_rooms',
|
name: "workadventure_nb_rooms",
|
||||||
help: 'Number of active rooms'
|
help: "Number of active rooms",
|
||||||
});
|
});
|
||||||
this.nbClientsGauge = new Gauge({
|
this.nbClientsGauge = new Gauge({
|
||||||
name: 'workadventure_nb_sockets',
|
name: "workadventure_nb_sockets",
|
||||||
help: 'Number of connected sockets',
|
help: "Number of connected sockets",
|
||||||
labelNames: [ ]
|
labelNames: [],
|
||||||
});
|
});
|
||||||
this.nbClientsPerRoomGauge = new Gauge({
|
this.nbClientsPerRoomGauge = new Gauge({
|
||||||
name: 'workadventure_nb_clients_per_room',
|
name: "workadventure_nb_clients_per_room",
|
||||||
help: 'Number of clients per room',
|
help: "Number of clients per room",
|
||||||
labelNames: [ 'room' ]
|
labelNames: ["room"],
|
||||||
});
|
});
|
||||||
|
|
||||||
this.nbGroupsPerRoomCounter = new Counter({
|
this.nbGroupsPerRoomCounter = new Counter({
|
||||||
name: 'workadventure_counter_groups_per_room',
|
name: "workadventure_counter_groups_per_room",
|
||||||
help: 'Counter of groups per room',
|
help: "Counter of groups per room",
|
||||||
labelNames: [ 'room' ]
|
labelNames: ["room"],
|
||||||
});
|
});
|
||||||
this.nbGroupsPerRoomGauge = new Gauge({
|
this.nbGroupsPerRoomGauge = new Gauge({
|
||||||
name: 'workadventure_nb_groups_per_room',
|
name: "workadventure_nb_groups_per_room",
|
||||||
help: 'Number of groups per room',
|
help: "Number of groups per room",
|
||||||
labelNames: [ 'room' ]
|
labelNames: ["room"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,15 +52,6 @@ class GaugeManager {
|
|||||||
this.nbClientsGauge.dec();
|
this.nbClientsGauge.dec();
|
||||||
this.nbClientsPerRoomGauge.dec({ room: roomId });
|
this.nbClientsPerRoomGauge.dec({ room: roomId });
|
||||||
}
|
}
|
||||||
|
|
||||||
incNbGroupsPerRoomGauge(roomId: string): void {
|
|
||||||
this.nbGroupsPerRoomCounter.inc({ room: roomId })
|
|
||||||
this.nbGroupsPerRoomGauge.inc({ room: roomId })
|
|
||||||
}
|
|
||||||
|
|
||||||
decNbGroupsPerRoomGauge(roomId: string): void {
|
|
||||||
this.nbGroupsPerRoomGauge.dec({ room: roomId })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const gaugeManager = new GaugeManager();
|
export const gaugeManager = new GaugeManager();
|
||||||
|
1
back/src/Services/LocalUrlError.ts
Normal file
1
back/src/Services/LocalUrlError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export class LocalUrlError extends Error {}
|
0
back/src/Services/Logger.ts
Normal file
0
back/src/Services/Logger.ts
Normal file
70
back/src/Services/MapFetcher.ts
Normal file
70
back/src/Services/MapFetcher.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import Axios from "axios";
|
||||||
|
import ipaddr from "ipaddr.js";
|
||||||
|
import { Resolver } from "dns";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import { LocalUrlError } from "./LocalUrlError";
|
||||||
|
import { ITiledMap } from "@workadventure/tiled-map-type-guard";
|
||||||
|
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
|
||||||
|
import { STORE_VARIABLES_FOR_LOCAL_MAPS } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
class MapFetcher {
|
||||||
|
async fetchMap(mapUrl: string): Promise<ITiledMap> {
|
||||||
|
// Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)
|
||||||
|
|
||||||
|
if ((await this.isLocalUrl(mapUrl)) && !STORE_VARIABLES_FOR_LOCAL_MAPS) {
|
||||||
|
throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that
|
||||||
|
// returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially
|
||||||
|
// target to different servers (and one could trick Axios.get into loading resources on the internal network
|
||||||
|
// despite isLocalUrl checking that.
|
||||||
|
// We can deem this problem not that important because:
|
||||||
|
// - We make sure we are only passing "GET" requests
|
||||||
|
// - The result of the query is never displayed to the end user
|
||||||
|
const res = await Axios.get(mapUrl, {
|
||||||
|
maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger
|
||||||
|
timeout: 10000, // Timeout after 10 seconds
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isTiledMap(res.data)) {
|
||||||
|
//TODO fixme
|
||||||
|
//throw new Error("Invalid map format for map " + mapUrl);
|
||||||
|
console.error("Invalid map format for map " + mapUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the domain name is localhost of *.localhost
|
||||||
|
* Returns true if the domain name resolves to an IP address that is "private" (like 10.x.x.x or 192.168.x.x)
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
async isLocalUrl(url: string): Promise<boolean> {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
if (urlObj.hostname === "localhost" || urlObj.hostname.endsWith(".localhost")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let addresses = [];
|
||||||
|
if (!ipaddr.isValid(urlObj.hostname)) {
|
||||||
|
const resolver = new Resolver();
|
||||||
|
addresses = await promisify(resolver.resolve).bind(resolver)(urlObj.hostname);
|
||||||
|
} else {
|
||||||
|
addresses = [urlObj.hostname];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const address of addresses) {
|
||||||
|
const addr = ipaddr.parse(address);
|
||||||
|
if (addr.range() !== "unicast") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapFetcher = new MapFetcher();
|
@ -1,5 +1,14 @@
|
|||||||
import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb";
|
import {
|
||||||
import {UserSocket} from "_Model/User";
|
BatchMessage,
|
||||||
|
BatchToPusherMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
|
ErrorMessage,
|
||||||
|
ServerToClientMessage,
|
||||||
|
SubToPusherMessage,
|
||||||
|
SubToPusherRoomMessage,
|
||||||
|
} from "../Messages/generated/messages_pb";
|
||||||
|
import { UserSocket } from "_Model/User";
|
||||||
|
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||||
|
|
||||||
export function emitError(Client: UserSocket, message: string): void {
|
export function emitError(Client: UserSocket, message: string): void {
|
||||||
const errorMessage = new ErrorMessage();
|
const errorMessage = new ErrorMessage();
|
||||||
@ -9,7 +18,43 @@ export function emitError(Client: UserSocket, message: string): void {
|
|||||||
serverToClientMessage.setErrormessage(errorMessage);
|
serverToClientMessage.setErrormessage(errorMessage);
|
||||||
|
|
||||||
//if (!Client.disconnecting) {
|
//if (!Client.disconnecting) {
|
||||||
Client.write(serverToClientMessage);
|
Client.write(serverToClientMessage);
|
||||||
|
//}
|
||||||
|
console.warn(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitErrorOnRoomSocket(Client: RoomSocket, message: string): void {
|
||||||
|
console.error(message);
|
||||||
|
|
||||||
|
const errorMessage = new ErrorMessage();
|
||||||
|
errorMessage.setMessage(message);
|
||||||
|
|
||||||
|
const subToPusherRoomMessage = new SubToPusherRoomMessage();
|
||||||
|
subToPusherRoomMessage.setErrormessage(errorMessage);
|
||||||
|
|
||||||
|
const batchToPusherMessage = new BatchToPusherRoomMessage();
|
||||||
|
batchToPusherMessage.addPayload(subToPusherRoomMessage);
|
||||||
|
|
||||||
|
//if (!Client.disconnecting) {
|
||||||
|
Client.write(batchToPusherMessage);
|
||||||
|
//}
|
||||||
|
console.warn(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitErrorOnZoneSocket(Client: ZoneSocket, message: string): void {
|
||||||
|
console.error(message);
|
||||||
|
|
||||||
|
const errorMessage = new ErrorMessage();
|
||||||
|
errorMessage.setMessage(message);
|
||||||
|
|
||||||
|
const subToPusherMessage = new SubToPusherMessage();
|
||||||
|
subToPusherMessage.setErrormessage(errorMessage);
|
||||||
|
|
||||||
|
const batchToPusherMessage = new BatchToPusherMessage();
|
||||||
|
batchToPusherMessage.addPayload(subToPusherMessage);
|
||||||
|
|
||||||
|
//if (!Client.disconnecting) {
|
||||||
|
Client.write(batchToPusherMessage);
|
||||||
//}
|
//}
|
||||||
console.warn(message);
|
console.warn(message);
|
||||||
}
|
}
|
||||||
|
23
back/src/Services/RedisClient.ts
Normal file
23
back/src/Services/RedisClient.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ClientOpts, createClient, RedisClient } from "redis";
|
||||||
|
import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
let redisClient: RedisClient | null = null;
|
||||||
|
|
||||||
|
if (REDIS_HOST !== undefined) {
|
||||||
|
const config: ClientOpts = {
|
||||||
|
host: REDIS_HOST,
|
||||||
|
port: REDIS_PORT,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (REDIS_PASSWORD) {
|
||||||
|
config.password = REDIS_PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
redisClient = createClient(config);
|
||||||
|
|
||||||
|
redisClient.on("error", (err) => {
|
||||||
|
console.error("Error connecting to Redis:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { redisClient };
|
43
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
43
back/src/Services/Repository/RedisVariablesRepository.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { promisify } from "util";
|
||||||
|
import { RedisClient } from "redis";
|
||||||
|
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class in charge of saving/loading variables from the data store
|
||||||
|
*/
|
||||||
|
export class RedisVariablesRepository implements VariablesRepositoryInterface {
|
||||||
|
private readonly hgetall: OmitThisParameter<(arg1: string) => Promise<{ [p: string]: string }>>;
|
||||||
|
private readonly hset: OmitThisParameter<(arg1: [string, ...string[]]) => Promise<number>>;
|
||||||
|
private readonly hdel: OmitThisParameter<(arg1: string, arg2: string) => Promise<number>>;
|
||||||
|
|
||||||
|
constructor(private redisClient: RedisClient) {
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
this.hgetall = promisify(redisClient.hgetall).bind(redisClient);
|
||||||
|
this.hset = promisify(redisClient.hset).bind(redisClient);
|
||||||
|
this.hdel = promisify(redisClient.hdel).bind(redisClient);
|
||||||
|
/* eslint-enable @typescript-eslint/unbound-method */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all variables for a room.
|
||||||
|
*
|
||||||
|
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
||||||
|
*/
|
||||||
|
async loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
||||||
|
return this.hgetall(roomUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
||||||
|
// The value is passed to JSON.stringify client side. If value is "undefined", JSON.stringify returns "undefined"
|
||||||
|
// which is translated to empty string when fetching the value in the pusher.
|
||||||
|
// Therefore, empty string server side == undefined client side.
|
||||||
|
if (value === "") {
|
||||||
|
return this.hdel(roomUrl, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: SLOW WRITING EVERY 2 SECONDS WITH A TIMEOUT
|
||||||
|
|
||||||
|
// @ts-ignore See https://stackoverflow.com/questions/63539317/how-do-i-use-hmset-with-node-promisify
|
||||||
|
return this.hset(roomUrl, key, value);
|
||||||
|
}
|
||||||
|
}
|
14
back/src/Services/Repository/VariablesRepository.ts
Normal file
14
back/src/Services/Repository/VariablesRepository.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { RedisVariablesRepository } from "./RedisVariablesRepository";
|
||||||
|
import { redisClient } from "../RedisClient";
|
||||||
|
import { VoidVariablesRepository } from "./VoidVariablesRepository";
|
||||||
|
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||||
|
|
||||||
|
let variablesRepository: VariablesRepositoryInterface;
|
||||||
|
if (!redisClient) {
|
||||||
|
console.warn("WARNING: Redis isnot configured. No variables will be persisted.");
|
||||||
|
variablesRepository = new VoidVariablesRepository();
|
||||||
|
} else {
|
||||||
|
variablesRepository = new RedisVariablesRepository(redisClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { variablesRepository };
|
10
back/src/Services/Repository/VariablesRepositoryInterface.ts
Normal file
10
back/src/Services/Repository/VariablesRepositoryInterface.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface VariablesRepositoryInterface {
|
||||||
|
/**
|
||||||
|
* Load all variables for a room.
|
||||||
|
*
|
||||||
|
* Note: in Redis, variables are stored in a hashmap and the key is the roomUrl
|
||||||
|
*/
|
||||||
|
loadVariables(roomUrl: string): Promise<{ [key: string]: string }>;
|
||||||
|
|
||||||
|
saveVariable(roomUrl: string, key: string, value: string): Promise<number>;
|
||||||
|
}
|
14
back/src/Services/Repository/VoidVariablesRepository.ts
Normal file
14
back/src/Services/Repository/VoidVariablesRepository.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock class in charge of NOT saving/loading variables from the data store
|
||||||
|
*/
|
||||||
|
export class VoidVariablesRepository implements VariablesRepositoryInterface {
|
||||||
|
loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
|
||||||
|
return Promise.resolve({});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
|
||||||
|
return Promise.resolve(0);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import {GameRoom} from "../Model/GameRoom";
|
import { GameRoom } from "../Model/GameRoom";
|
||||||
import {
|
import {
|
||||||
ItemEventMessage,
|
ItemEventMessage,
|
||||||
ItemStateMessage,
|
ItemStateMessage,
|
||||||
@ -26,45 +26,51 @@ import {
|
|||||||
GroupLeftZoneMessage,
|
GroupLeftZoneMessage,
|
||||||
WorldFullWarningMessage,
|
WorldFullWarningMessage,
|
||||||
UserLeftZoneMessage,
|
UserLeftZoneMessage,
|
||||||
BanUserMessage, RefreshRoomMessage,
|
EmoteEventMessage,
|
||||||
|
BanUserMessage,
|
||||||
|
RefreshRoomMessage,
|
||||||
|
EmotePromptMessage,
|
||||||
|
VariableMessage,
|
||||||
|
BatchToPusherRoomMessage,
|
||||||
|
SubToPusherRoomMessage,
|
||||||
} from "../Messages/generated/messages_pb";
|
} from "../Messages/generated/messages_pb";
|
||||||
import {User, UserSocket} from "../Model/User";
|
import { User, UserSocket } from "../Model/User";
|
||||||
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
|
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils";
|
||||||
import {Group} from "../Model/Group";
|
import { Group } from "../Model/Group";
|
||||||
import {cpuTracker} from "./CpuTracker";
|
import { cpuTracker } from "./CpuTracker";
|
||||||
import {
|
import {
|
||||||
GROUP_RADIUS,
|
GROUP_RADIUS,
|
||||||
JITSI_ISS,
|
JITSI_ISS,
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
SECRET_JITSI_KEY,
|
SECRET_JITSI_KEY,
|
||||||
TURN_STATIC_AUTH_SECRET
|
TURN_STATIC_AUTH_SECRET,
|
||||||
} from "../Enum/EnvironmentVariable";
|
} from "../Enum/EnvironmentVariable";
|
||||||
import {Movable} from "../Model/Movable";
|
import { Movable } from "../Model/Movable";
|
||||||
import {PositionInterface} from "../Model/PositionInterface";
|
import { PositionInterface } from "../Model/PositionInterface";
|
||||||
import Jwt from "jsonwebtoken";
|
import Jwt from "jsonwebtoken";
|
||||||
import {JITSI_URL} from "../Enum/EnvironmentVariable";
|
import { JITSI_URL } from "../Enum/EnvironmentVariable";
|
||||||
import {clientEventsEmitter} from "./ClientEventsEmitter";
|
import { clientEventsEmitter } from "./ClientEventsEmitter";
|
||||||
import {gaugeManager} from "./GaugeManager";
|
import { gaugeManager } from "./GaugeManager";
|
||||||
import {ZoneSocket} from "../RoomManager";
|
import { RoomSocket, ZoneSocket } from "../RoomManager";
|
||||||
import {Zone} from "_Model/Zone";
|
import { Zone } from "_Model/Zone";
|
||||||
import Debug from "debug";
|
import Debug from "debug";
|
||||||
import {Admin} from "_Model/Admin";
|
import { Admin } from "_Model/Admin";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
const debug = Debug("sockermanager");
|
||||||
const debug = Debug('sockermanager');
|
|
||||||
|
|
||||||
function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void {
|
function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void {
|
||||||
// TODO: should we batch those every 100ms?
|
// TODO: should we batch those every 100ms?
|
||||||
const batchMessage = new BatchToPusherMessage();
|
const batchMessage = new BatchToPusherMessage();
|
||||||
batchMessage.addPayload(subMessage);
|
batchMessage.addPayload(subMessage);
|
||||||
|
|
||||||
|
|
||||||
socket.write(batchMessage);
|
socket.write(batchMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SocketManager {
|
export class SocketManager {
|
||||||
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
|
//private rooms = new Map<string, GameRoom>();
|
||||||
|
// List of rooms in process of loading.
|
||||||
|
private roomsPromises = new Map<string, PromiseLike<GameRoom>>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
||||||
@ -75,16 +81,18 @@ export class SocketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleJoinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> {
|
public async handleJoinRoom(
|
||||||
|
socket: UserSocket,
|
||||||
|
joinRoomMessage: JoinRoomMessage
|
||||||
|
): Promise<{ room: GameRoom; user: User }> {
|
||||||
//join new previous room
|
//join new previous room
|
||||||
const {room, user} = await this.joinRoom(socket, joinRoomMessage);
|
const { room, user } = await this.joinRoom(socket, joinRoomMessage);
|
||||||
|
|
||||||
if (!socket.writable) {
|
if (!socket.writable) {
|
||||||
console.warn('Socket was aborted');
|
console.warn("Socket was aborted");
|
||||||
return {
|
return {
|
||||||
room,
|
room,
|
||||||
user
|
user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const roomJoinedMessage = new RoomJoinedMessage();
|
const roomJoinedMessage = new RoomJoinedMessage();
|
||||||
@ -98,6 +106,16 @@ export class SocketManager {
|
|||||||
roomJoinedMessage.addItem(itemStateMessage);
|
roomJoinedMessage.addItem(itemStateMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const variables = await room.getVariablesForTags(user.tags);
|
||||||
|
|
||||||
|
for (const [name, value] of variables.entries()) {
|
||||||
|
const variableMessage = new VariableMessage();
|
||||||
|
variableMessage.setName(name);
|
||||||
|
variableMessage.setValue(value);
|
||||||
|
|
||||||
|
roomJoinedMessage.addVariable(variableMessage);
|
||||||
|
}
|
||||||
|
|
||||||
roomJoinedMessage.setCurrentuserid(user.id);
|
roomJoinedMessage.setCurrentuserid(user.id);
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
@ -106,37 +124,30 @@ export class SocketManager {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
room,
|
room,
|
||||||
user
|
user,
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) {
|
||||||
try {
|
const userMoves = userMovesMessage.toObject();
|
||||||
const userMoves = userMovesMessage.toObject();
|
const position = userMovesMessage.getPosition();
|
||||||
const position = userMovesMessage.getPosition();
|
|
||||||
|
|
||||||
// If CPU is high, let's drop messages of users moving (we will only dispatch the final position)
|
// If CPU is high, let's drop messages of users moving (we will only dispatch the final position)
|
||||||
if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) {
|
if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (position === undefined) {
|
|
||||||
throw new Error('Position not found in message');
|
|
||||||
}
|
|
||||||
const viewport = userMoves.viewport;
|
|
||||||
if (viewport === undefined) {
|
|
||||||
throw new Error('Viewport not found in message');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// update position in the world
|
|
||||||
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
|
||||||
//room.setViewport(client, client.viewport);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('An error occurred on "user_position" event');
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (position === undefined) {
|
||||||
|
throw new Error("Position not found in message");
|
||||||
|
}
|
||||||
|
const viewport = userMoves.viewport;
|
||||||
|
if (viewport === undefined) {
|
||||||
|
throw new Error("Viewport not found in message");
|
||||||
|
}
|
||||||
|
|
||||||
|
// update position in the world
|
||||||
|
room.updatePosition(user, ProtobufUtils.toPointInterface(position));
|
||||||
|
//room.setViewport(client, client.viewport);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Useless now, will be useful again if we allow editing details in game
|
// Useless now, will be useful again if we allow editing details in game
|
||||||
@ -155,39 +166,37 @@ export class SocketManager {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
|
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
|
||||||
try {
|
room.setSilent(user, silentMessage.getSilent());
|
||||||
room.setSilent(user, silentMessage.getSilent());
|
|
||||||
} catch (e) {
|
|
||||||
console.error('An error occurred on "handleSilentMessage"');
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) {
|
handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) {
|
||||||
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
|
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
|
||||||
|
|
||||||
try {
|
const subMessage = new SubMessage();
|
||||||
const subMessage = new SubMessage();
|
subMessage.setItemeventmessage(itemEventMessage);
|
||||||
subMessage.setItemeventmessage(itemEventMessage);
|
|
||||||
|
|
||||||
// Let's send the event without using the SocketIO room.
|
// Let's send the event without using the SocketIO room.
|
||||||
// TODO: move this in the GameRoom class.
|
// TODO: move this in the GameRoom class.
|
||||||
for (const user of room.getUsers().values()) {
|
for (const user of room.getUsers().values()) {
|
||||||
user.emitInBatch(subMessage);
|
user.emitInBatch(subMessage);
|
||||||
}
|
|
||||||
|
|
||||||
room.setItemState(itemEvent.itemId, itemEvent.state);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('An error occurred on "item_event"');
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
room.setItemState(itemEvent.itemId, itemEvent.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVariableEvent(room: GameRoom, user: User, variableMessage: VariableMessage): Promise<void> {
|
||||||
|
return room.setVariable(variableMessage.getName(), variableMessage.getValue(), user);
|
||||||
}
|
}
|
||||||
|
|
||||||
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
emitVideo(room: GameRoom, user: User, data: WebRtcSignalToServerMessage): void {
|
||||||
//send only at user
|
//send only at user
|
||||||
const remoteUser = room.getUsers().get(data.getReceiverid());
|
const remoteUser = room.getUsers().get(data.getReceiverid());
|
||||||
if (remoteUser === undefined) {
|
if (remoteUser === undefined) {
|
||||||
console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition.");
|
console.warn(
|
||||||
|
"While exchanging a WebRTC signal: client with id ",
|
||||||
|
data.getReceiverid(),
|
||||||
|
" does not exist. This might be a race condition."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,8 +204,8 @@ export class SocketManager {
|
|||||||
webrtcSignalToClient.setUserid(user.id);
|
webrtcSignalToClient.setUserid(user.id);
|
||||||
webrtcSignalToClient.setSignal(data.getSignal());
|
webrtcSignalToClient.setSignal(data.getSignal());
|
||||||
// TODO: only compute credentials if data.signal.type === "offer"
|
// TODO: only compute credentials if data.signal.type === "offer"
|
||||||
if (TURN_STATIC_AUTH_SECRET !== '') {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
|
||||||
webrtcSignalToClient.setWebrtcusername(username);
|
webrtcSignalToClient.setWebrtcusername(username);
|
||||||
webrtcSignalToClient.setWebrtcpassword(password);
|
webrtcSignalToClient.setWebrtcpassword(password);
|
||||||
}
|
}
|
||||||
@ -205,7 +214,7 @@ export class SocketManager {
|
|||||||
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
|
serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient);
|
||||||
|
|
||||||
//if (!client.disconnecting) {
|
//if (!client.disconnecting) {
|
||||||
remoteUser.socket.write(serverToClientMessage);
|
remoteUser.socket.write(serverToClientMessage);
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +222,11 @@ export class SocketManager {
|
|||||||
//send only at user
|
//send only at user
|
||||||
const remoteUser = room.getUsers().get(data.getReceiverid());
|
const remoteUser = room.getUsers().get(data.getReceiverid());
|
||||||
if (remoteUser === undefined) {
|
if (remoteUser === undefined) {
|
||||||
console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition.");
|
console.warn(
|
||||||
|
"While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ",
|
||||||
|
data.getReceiverid(),
|
||||||
|
" does not exist. This might be a race condition."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,8 +234,8 @@ export class SocketManager {
|
|||||||
webrtcSignalToClient.setUserid(user.id);
|
webrtcSignalToClient.setUserid(user.id);
|
||||||
webrtcSignalToClient.setSignal(data.getSignal());
|
webrtcSignalToClient.setSignal(data.getSignal());
|
||||||
// TODO: only compute credentials if data.signal.type === "offer"
|
// TODO: only compute credentials if data.signal.type === "offer"
|
||||||
if (TURN_STATIC_AUTH_SECRET !== '') {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
|
||||||
webrtcSignalToClient.setWebrtcusername(username);
|
webrtcSignalToClient.setWebrtcusername(username);
|
||||||
webrtcSignalToClient.setWebrtcpassword(password);
|
webrtcSignalToClient.setWebrtcpassword(password);
|
||||||
}
|
}
|
||||||
@ -231,48 +244,62 @@ export class SocketManager {
|
|||||||
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
|
serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient);
|
||||||
|
|
||||||
//if (!client.disconnecting) {
|
//if (!client.disconnecting) {
|
||||||
remoteUser.socket.write(serverToClientMessage);
|
remoteUser.socket.write(serverToClientMessage);
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
leaveRoom(room: GameRoom, user: User){
|
leaveRoom(room: GameRoom, user: User) {
|
||||||
// leave previous room and world
|
// leave previous room and world
|
||||||
try {
|
try {
|
||||||
//user leave previous world
|
//user leave previous world
|
||||||
room.leave(user);
|
room.leave(user);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
this.rooms.delete(room.roomId);
|
this.roomsPromises.delete(room.roomUrl);
|
||||||
gaugeManager.decNbRoomGauge();
|
gaugeManager.decNbRoomGauge();
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clientEventsEmitter.emitClientLeave(user.uuid, room.roomId);
|
clientEventsEmitter.emitClientLeave(user.uuid, room.roomUrl);
|
||||||
console.log('A user left');
|
console.log("A user left");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
async getOrCreateRoom(roomId: string): Promise<GameRoom> {
|
||||||
//check and create new world for a room
|
//check and create new room
|
||||||
let world = this.rooms.get(roomId)
|
let roomPromise = this.roomsPromises.get(roomId);
|
||||||
if(world === undefined){
|
if (roomPromise === undefined) {
|
||||||
world = new GameRoom(
|
roomPromise = GameRoom.create(
|
||||||
roomId,
|
roomId,
|
||||||
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
(user: User, group: Group) => this.joinWebRtcRoom(user, group),
|
||||||
(user: User, group: Group) => this.disConnectedUser(user, group),
|
(user: User, group: Group) => this.disConnectedUser(user, group),
|
||||||
MINIMUM_DISTANCE,
|
MINIMUM_DISTANCE,
|
||||||
GROUP_RADIUS,
|
GROUP_RADIUS,
|
||||||
(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener),
|
(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) =>
|
||||||
(thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener),
|
this.onZoneEnter(thing, fromZone, listener),
|
||||||
(thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener)
|
(thing: Movable, position: PositionInterface, listener: ZoneSocket) =>
|
||||||
);
|
this.onClientMove(thing, position, listener),
|
||||||
gaugeManager.incNbRoomGauge();
|
(thing: Movable, newZone: Zone | null, listener: ZoneSocket) =>
|
||||||
this.rooms.set(roomId, world);
|
this.onClientLeave(thing, newZone, listener),
|
||||||
|
(emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) =>
|
||||||
|
this.onEmote(emoteEventMessage, listener)
|
||||||
|
)
|
||||||
|
.then((gameRoom) => {
|
||||||
|
gaugeManager.incNbRoomGauge();
|
||||||
|
return gameRoom;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
this.roomsPromises.delete(roomId);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
this.roomsPromises.set(roomId, roomPromise);
|
||||||
}
|
}
|
||||||
return Promise.resolve(world)
|
return roomPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async joinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> {
|
private async joinRoom(
|
||||||
|
socket: UserSocket,
|
||||||
|
joinRoomMessage: JoinRoomMessage
|
||||||
|
): Promise<{ room: GameRoom; user: User }> {
|
||||||
const roomId = joinRoomMessage.getRoomid();
|
const roomId = joinRoomMessage.getRoomid();
|
||||||
|
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
const room = await socketManager.getOrCreateRoom(roomId);
|
||||||
@ -281,21 +308,25 @@ export class SocketManager {
|
|||||||
const user = room.join(socket, joinRoomMessage);
|
const user = room.join(socket, joinRoomMessage);
|
||||||
|
|
||||||
clientEventsEmitter.emitClientJoin(user.uuid, roomId);
|
clientEventsEmitter.emitClientJoin(user.uuid, roomId);
|
||||||
console.log(new Date().toISOString() + ' A user joined');
|
console.log(new Date().toISOString() + " A user joined");
|
||||||
return {room, user};
|
return { room, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
private onZoneEnter(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) {
|
private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) {
|
||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
const userJoinedZoneMessage = new UserJoinedZoneMessage();
|
const userJoinedZoneMessage = new UserJoinedZoneMessage();
|
||||||
if (!Number.isInteger(thing.id)) {
|
if (!Number.isInteger(thing.id)) {
|
||||||
throw new Error('clientUser.userId is not an integer '+thing.id);
|
throw new Error("clientUser.userId is not an integer " + thing.id);
|
||||||
}
|
}
|
||||||
userJoinedZoneMessage.setUserid(thing.id);
|
userJoinedZoneMessage.setUserid(thing.id);
|
||||||
|
userJoinedZoneMessage.setUseruuid(thing.uuid);
|
||||||
userJoinedZoneMessage.setName(thing.name);
|
userJoinedZoneMessage.setName(thing.name);
|
||||||
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
||||||
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
||||||
userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone));
|
userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone));
|
||||||
|
if (thing.visitCardUrl) {
|
||||||
|
userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl);
|
||||||
|
}
|
||||||
userJoinedZoneMessage.setCompanion(thing.companion);
|
userJoinedZoneMessage.setCompanion(thing.companion);
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
const subMessage = new SubToPusherMessage();
|
||||||
@ -306,11 +337,11 @@ export class SocketManager {
|
|||||||
} else if (thing instanceof Group) {
|
} else if (thing instanceof Group) {
|
||||||
this.emitCreateUpdateGroupEvent(listener, fromZone, thing);
|
this.emitCreateUpdateGroupEvent(listener, fromZone, thing);
|
||||||
} else {
|
} else {
|
||||||
console.error('Unexpected type for Movable.');
|
console.error("Unexpected type for Movable.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClientMove(thing: Movable, position:PositionInterface, listener: ZoneSocket): void {
|
private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void {
|
||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
const userMovedMessage = new UserMovedMessage();
|
const userMovedMessage = new UserMovedMessage();
|
||||||
userMovedMessage.setUserid(thing.id);
|
userMovedMessage.setUserid(thing.id);
|
||||||
@ -325,21 +356,28 @@ export class SocketManager {
|
|||||||
} else if (thing instanceof Group) {
|
} else if (thing instanceof Group) {
|
||||||
this.emitCreateUpdateGroupEvent(listener, null, thing);
|
this.emitCreateUpdateGroupEvent(listener, null, thing);
|
||||||
} else {
|
} else {
|
||||||
console.error('Unexpected type for Movable.');
|
console.error("Unexpected type for Movable.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onClientLeave(thing: Movable, newZone: Zone|null, listener: ZoneSocket) {
|
private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) {
|
||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
this.emitUserLeftEvent(listener, thing.id, newZone);
|
this.emitUserLeftEvent(listener, thing.id, newZone);
|
||||||
} else if (thing instanceof Group) {
|
} else if (thing instanceof Group) {
|
||||||
this.emitDeleteGroupEvent(listener, thing.getId(), newZone);
|
this.emitDeleteGroupEvent(listener, thing.getId(), newZone);
|
||||||
} else {
|
} else {
|
||||||
console.error('Unexpected type for Movable.');
|
console.error("Unexpected type for Movable.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void {
|
private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) {
|
||||||
|
const subMessage = new SubToPusherMessage();
|
||||||
|
subMessage.setEmoteeventmessage(emoteEventMessage);
|
||||||
|
|
||||||
|
emitZoneMessage(subMessage, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void {
|
||||||
const position = group.getPosition();
|
const position = group.getPosition();
|
||||||
const pointMessage = new PointMessage();
|
const pointMessage = new PointMessage();
|
||||||
pointMessage.setX(Math.floor(position.x));
|
pointMessage.setX(Math.floor(position.x));
|
||||||
@ -357,7 +395,7 @@ export class SocketManager {
|
|||||||
//client.emitInBatch(subMessage);
|
//client.emitInBatch(subMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone|null): void {
|
private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void {
|
||||||
const groupDeleteMessage = new GroupLeftZoneMessage();
|
const groupDeleteMessage = new GroupLeftZoneMessage();
|
||||||
groupDeleteMessage.setGroupid(groupId);
|
groupDeleteMessage.setGroupid(groupId);
|
||||||
groupDeleteMessage.setTozone(this.toProtoZone(newZone));
|
groupDeleteMessage.setTozone(this.toProtoZone(newZone));
|
||||||
@ -369,7 +407,7 @@ export class SocketManager {
|
|||||||
//user.emitInBatch(subMessage);
|
//user.emitInBatch(subMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone|null): void {
|
private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void {
|
||||||
const userLeftMessage = new UserLeftZoneMessage();
|
const userLeftMessage = new UserLeftZoneMessage();
|
||||||
userLeftMessage.setUserid(userId);
|
userLeftMessage.setUserid(userId);
|
||||||
userLeftMessage.setTozone(this.toProtoZone(newZone));
|
userLeftMessage.setTozone(this.toProtoZone(newZone));
|
||||||
@ -380,7 +418,7 @@ export class SocketManager {
|
|||||||
emitZoneMessage(subMessage, client);
|
emitZoneMessage(subMessage, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toProtoZone(zone: Zone|null): ProtoZone|undefined {
|
private toProtoZone(zone: Zone | null): ProtoZone | undefined {
|
||||||
if (zone !== null) {
|
if (zone !== null) {
|
||||||
const zoneMessage = new ProtoZone();
|
const zoneMessage = new ProtoZone();
|
||||||
zoneMessage.setX(zone.x);
|
zoneMessage.setX(zone.x);
|
||||||
@ -391,7 +429,6 @@ export class SocketManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private joinWebRtcRoom(user: User, group: Group) {
|
private joinWebRtcRoom(user: User, group: Group) {
|
||||||
|
|
||||||
for (const otherUser of group.getUsers()) {
|
for (const otherUser of group.getUsers()) {
|
||||||
if (user === otherUser) {
|
if (user === otherUser) {
|
||||||
continue;
|
continue;
|
||||||
@ -400,10 +437,9 @@ export class SocketManager {
|
|||||||
// Let's send 2 messages: one to the user joining the group and one to the other user
|
// Let's send 2 messages: one to the user joining the group and one to the other user
|
||||||
const webrtcStartMessage1 = new WebRtcStartMessage();
|
const webrtcStartMessage1 = new WebRtcStartMessage();
|
||||||
webrtcStartMessage1.setUserid(otherUser.id);
|
webrtcStartMessage1.setUserid(otherUser.id);
|
||||||
webrtcStartMessage1.setName(otherUser.name);
|
|
||||||
webrtcStartMessage1.setInitiator(true);
|
webrtcStartMessage1.setInitiator(true);
|
||||||
if (TURN_STATIC_AUTH_SECRET !== '') {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET);
|
||||||
webrtcStartMessage1.setWebrtcusername(username);
|
webrtcStartMessage1.setWebrtcusername(username);
|
||||||
webrtcStartMessage1.setWebrtcpassword(password);
|
webrtcStartMessage1.setWebrtcpassword(password);
|
||||||
}
|
}
|
||||||
@ -411,17 +447,13 @@ export class SocketManager {
|
|||||||
const serverToClientMessage1 = new ServerToClientMessage();
|
const serverToClientMessage1 = new ServerToClientMessage();
|
||||||
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1);
|
||||||
|
|
||||||
//if (!user.socket.disconnecting) {
|
user.socket.write(serverToClientMessage1);
|
||||||
user.socket.write(serverToClientMessage1);
|
|
||||||
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
|
|
||||||
//}
|
|
||||||
|
|
||||||
const webrtcStartMessage2 = new WebRtcStartMessage();
|
const webrtcStartMessage2 = new WebRtcStartMessage();
|
||||||
webrtcStartMessage2.setUserid(user.id);
|
webrtcStartMessage2.setUserid(user.id);
|
||||||
webrtcStartMessage2.setName(user.name);
|
|
||||||
webrtcStartMessage2.setInitiator(false);
|
webrtcStartMessage2.setInitiator(false);
|
||||||
if (TURN_STATIC_AUTH_SECRET !== '') {
|
if (TURN_STATIC_AUTH_SECRET !== "") {
|
||||||
const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET);
|
const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET);
|
||||||
webrtcStartMessage2.setWebrtcusername(username);
|
webrtcStartMessage2.setWebrtcusername(username);
|
||||||
webrtcStartMessage2.setWebrtcpassword(password);
|
webrtcStartMessage2.setWebrtcpassword(password);
|
||||||
}
|
}
|
||||||
@ -429,11 +461,7 @@ export class SocketManager {
|
|||||||
const serverToClientMessage2 = new ServerToClientMessage();
|
const serverToClientMessage2 = new ServerToClientMessage();
|
||||||
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2);
|
||||||
|
|
||||||
//if (!otherUser.socket.disconnecting) {
|
otherUser.socket.write(serverToClientMessage2);
|
||||||
otherUser.socket.write(serverToClientMessage2);
|
|
||||||
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
|
|
||||||
//}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,17 +470,17 @@ export class SocketManager {
|
|||||||
* and the Coturn server.
|
* and the Coturn server.
|
||||||
* The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey`
|
* The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey`
|
||||||
*/
|
*/
|
||||||
private getTURNCredentials(name: string, secret: string): {username: string, password: string} {
|
private getTURNCredentials(name: string, secret: string): { username: string; password: string } {
|
||||||
const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours
|
const unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours
|
||||||
const username = [unixTimeStamp, name].join(':');
|
const username = [unixTimeStamp, name].join(":");
|
||||||
const hmac = crypto.createHmac('sha1', secret);
|
const hmac = crypto.createHmac("sha1", secret);
|
||||||
hmac.setEncoding('base64');
|
hmac.setEncoding("base64");
|
||||||
hmac.write(username);
|
hmac.write(username);
|
||||||
hmac.end();
|
hmac.end();
|
||||||
const password = hmac.read();
|
const password = hmac.read();
|
||||||
return {
|
return {
|
||||||
username: username,
|
username: username,
|
||||||
password: password
|
password: password,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,10 +503,9 @@ export class SocketManager {
|
|||||||
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
|
serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1);
|
||||||
|
|
||||||
//if (!otherUser.socket.disconnecting) {
|
//if (!otherUser.socket.disconnecting) {
|
||||||
otherUser.socket.write(serverToClientMessage1);
|
otherUser.socket.write(serverToClientMessage1);
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
|
||||||
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
|
const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage();
|
||||||
webrtcDisconnectMessage2.setUserid(otherUser.id);
|
webrtcDisconnectMessage2.setUserid(otherUser.id);
|
||||||
|
|
||||||
@ -486,57 +513,53 @@ export class SocketManager {
|
|||||||
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
|
serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2);
|
||||||
|
|
||||||
//if (!user.socket.disconnecting) {
|
//if (!user.socket.disconnecting) {
|
||||||
user.socket.write(serverToClientMessage2);
|
user.socket.write(serverToClientMessage2);
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) {
|
emitPlayGlobalMessage(room: GameRoom, playGlobalMessage: PlayGlobalMessage) {
|
||||||
try {
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
|
||||||
serverToClientMessage.setPlayglobalmessage(playGlobalMessage);
|
|
||||||
|
|
||||||
for (const [id, user] of room.getUsers().entries()) {
|
for (const [id, user] of room.getUsers().entries()) {
|
||||||
user.socket.write(serverToClientMessage);
|
user.socket.write(serverToClientMessage);
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('An error occurred on "emitPlayGlobalMessage" event');
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWorlds(): Map<string, GameRoom> {
|
public getWorlds(): Map<string, PromiseLike<GameRoom>> {
|
||||||
return this.rooms;
|
return this.roomsPromises;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) {
|
||||||
const room = queryJitsiJwtMessage.getJitsiroom();
|
const room = queryJitsiJwtMessage.getJitsiroom();
|
||||||
const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead.
|
const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead.
|
||||||
|
|
||||||
if (SECRET_JITSI_KEY === '') {
|
if (SECRET_JITSI_KEY === "") {
|
||||||
throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.');
|
throw new Error("You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's see if the current client has
|
// Let's see if the current client has
|
||||||
const isAdmin = user.tags.includes(tag);
|
const isAdmin = user.tags.includes(tag);
|
||||||
|
|
||||||
const jwt = Jwt.sign({
|
const jwt = Jwt.sign(
|
||||||
"aud": "jitsi",
|
{
|
||||||
"iss": JITSI_ISS,
|
aud: "jitsi",
|
||||||
"sub": JITSI_URL,
|
iss: JITSI_ISS,
|
||||||
"room": room,
|
sub: JITSI_URL,
|
||||||
"moderator": isAdmin
|
room: room,
|
||||||
}, SECRET_JITSI_KEY, {
|
moderator: isAdmin,
|
||||||
expiresIn: '1d',
|
},
|
||||||
algorithm: "HS256",
|
SECRET_JITSI_KEY,
|
||||||
header:
|
{
|
||||||
{
|
expiresIn: "1d",
|
||||||
"alg": "HS256",
|
algorithm: "HS256",
|
||||||
"typ": "JWT"
|
header: {
|
||||||
}
|
alg: "HS256",
|
||||||
});
|
typ: "JWT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const sendJitsiJwtMessage = new SendJitsiJwtMessage();
|
const sendJitsiJwtMessage = new SendJitsiJwtMessage();
|
||||||
sendJitsiJwtMessage.setJitsiroom(room);
|
sendJitsiJwtMessage.setJitsiroom(room);
|
||||||
@ -548,7 +571,7 @@ export class SocketManager {
|
|||||||
user.socket.write(serverToClientMessage);
|
user.socket.write(serverToClientMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage){
|
public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) {
|
||||||
const sendUserMessage = new SendUserMessage();
|
const sendUserMessage = new SendUserMessage();
|
||||||
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
|
sendUserMessage.setMessage(sendUserMessageToSend.getMessage());
|
||||||
sendUserMessage.setType(sendUserMessageToSend.getType());
|
sendUserMessage.setType(sendUserMessageToSend.getType());
|
||||||
@ -558,7 +581,7 @@ export class SocketManager {
|
|||||||
user.socket.write(serverToClientMessage);
|
user.socket.write(serverToClientMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage){
|
public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) {
|
||||||
const banUserMessage = new BanUserMessage();
|
const banUserMessage = new BanUserMessage();
|
||||||
banUserMessage.setMessage(banUserMessageToSend.getMessage());
|
banUserMessage.setMessage(banUserMessageToSend.getMessage());
|
||||||
banUserMessage.setType(banUserMessageToSend.getType());
|
banUserMessage.setType(banUserMessageToSend.getType());
|
||||||
@ -575,11 +598,10 @@ export class SocketManager {
|
|||||||
}, 10000);
|
}, 10000);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void {
|
public async addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In addZoneListener, could not find room with id '" + roomId + "'");
|
throw new Error("In addZoneListener, could not find room with id '" + roomId + "'");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const things = room.addZoneListener(call, x, y);
|
const things = room.addZoneListener(call, x, y);
|
||||||
@ -590,9 +612,13 @@ export class SocketManager {
|
|||||||
if (thing instanceof User) {
|
if (thing instanceof User) {
|
||||||
const userJoinedMessage = new UserJoinedZoneMessage();
|
const userJoinedMessage = new UserJoinedZoneMessage();
|
||||||
userJoinedMessage.setUserid(thing.id);
|
userJoinedMessage.setUserid(thing.id);
|
||||||
|
userJoinedMessage.setUseruuid(thing.uuid);
|
||||||
userJoinedMessage.setName(thing.name);
|
userJoinedMessage.setName(thing.name);
|
||||||
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
|
||||||
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
|
||||||
|
if (thing.visitCardUrl) {
|
||||||
|
userJoinedMessage.setVisitcardurl(thing.visitCardUrl);
|
||||||
|
}
|
||||||
userJoinedMessage.setCompanion(thing.companion);
|
userJoinedMessage.setCompanion(thing.companion);
|
||||||
|
|
||||||
const subMessage = new SubToPusherMessage();
|
const subMessage = new SubToPusherMessage();
|
||||||
@ -616,16 +642,37 @@ export class SocketManager {
|
|||||||
call.write(batchMessage);
|
call.write(batchMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) {
|
async removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
throw new Error("In removeZoneListener, could not find room with id '" + roomId + "'");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
room.removeZoneListener(call, x, y);
|
room.removeZoneListener(call, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addRoomListener(call: RoomSocket, roomId: string) {
|
||||||
|
const room = await this.getOrCreateRoom(roomId);
|
||||||
|
if (!room) {
|
||||||
|
throw new Error("In addRoomListener, could not find room with id '" + roomId + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
room.addRoomListener(call);
|
||||||
|
|
||||||
|
const batchMessage = new BatchToPusherRoomMessage();
|
||||||
|
|
||||||
|
call.write(batchMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeRoomListener(call: RoomSocket, roomId: string) {
|
||||||
|
const room = await this.roomsPromises.get(roomId);
|
||||||
|
if (!room) {
|
||||||
|
throw new Error("In removeRoomListener, could not find room with id '" + roomId + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
room.removeRoomListener(call);
|
||||||
|
}
|
||||||
|
|
||||||
public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise<GameRoom> {
|
public async handleJoinAdminRoom(admin: Admin, roomId: string): Promise<GameRoom> {
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
const room = await socketManager.getOrCreateRoom(roomId);
|
||||||
|
|
||||||
@ -634,79 +681,102 @@ export class SocketManager {
|
|||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
public leaveAdminRoom(room: GameRoom, admin: Admin){
|
public leaveAdminRoom(room: GameRoom, admin: Admin) {
|
||||||
room.adminLeave(admin);
|
room.adminLeave(admin);
|
||||||
if (room.isEmpty()) {
|
if (room.isEmpty()) {
|
||||||
this.rooms.delete(room.roomId);
|
this.roomsPromises.delete(room.roomUrl);
|
||||||
gaugeManager.decNbRoomGauge();
|
gaugeManager.decNbRoomGauge();
|
||||||
debug('Room is empty. Deleting room "%s"', room.roomId);
|
debug('Room is empty. Deleting room "%s"', room.roomUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void {
|
public async sendAdminMessage(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In sendAdminMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In sendAdminMessage, could not find room with id '" +
|
||||||
|
roomId +
|
||||||
|
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipient = room.getUserByUuid(recipientUuid);
|
const recipients = room.getUsersByUuid(recipientUuid);
|
||||||
if (recipient === undefined) {
|
if (recipients.length === 0) {
|
||||||
console.error("In sendAdminMessage, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In sendAdminMessage, could not find user with id '" +
|
||||||
|
recipientUuid +
|
||||||
|
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendUserMessage = new SendUserMessage();
|
for (const recipient of recipients) {
|
||||||
sendUserMessage.setMessage(message);
|
const sendUserMessage = new SendUserMessage();
|
||||||
sendUserMessage.setType('ban'); //todo: is the type correct?
|
sendUserMessage.setMessage(message);
|
||||||
|
sendUserMessage.setType("ban"); //todo: is the type correct?
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
serverToClientMessage.setSendusermessage(sendUserMessage);
|
serverToClientMessage.setSendusermessage(sendUserMessage);
|
||||||
|
|
||||||
recipient.socket.write(serverToClientMessage);
|
recipient.socket.write(serverToClientMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public banUser(roomId: string, recipientUuid: string, message: string): void {
|
public async banUser(roomId: string, recipientUuid: string, message: string): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
console.error("In banUser, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In banUser, could not find room with id '" +
|
||||||
|
roomId +
|
||||||
|
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipient = room.getUserByUuid(recipientUuid);
|
const recipients = room.getUsersByUuid(recipientUuid);
|
||||||
if (recipient === undefined) {
|
if (recipients.length === 0) {
|
||||||
console.error("In banUser, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In banUser, could not find user with id '" +
|
||||||
|
recipientUuid +
|
||||||
|
"'. Maybe the user left the room a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's leave the room now.
|
for (const recipient of recipients) {
|
||||||
room.leave(recipient);
|
// Let's leave the room now.
|
||||||
|
room.leave(recipient);
|
||||||
|
|
||||||
const banUserMessage = new BanUserMessage();
|
const banUserMessage = new BanUserMessage();
|
||||||
banUserMessage.setMessage(message);
|
banUserMessage.setMessage(message);
|
||||||
banUserMessage.setType('banned');
|
banUserMessage.setType("banned");
|
||||||
|
|
||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
serverToClientMessage.setBanusermessage(banUserMessage);
|
serverToClientMessage.setBanusermessage(banUserMessage);
|
||||||
|
|
||||||
// Let's close the connection when the user is banned.
|
// Let's close the connection when the user is banned.
|
||||||
recipient.socket.write(serverToClientMessage);
|
recipient.socket.write(serverToClientMessage);
|
||||||
recipient.socket.end();
|
recipient.socket.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendAdminRoomMessage(roomId: string, message: string, type: string) {
|
||||||
sendAdminRoomMessage(roomId: string, message: string) {
|
const room = await this.roomsPromises.get(roomId);
|
||||||
const room = this.rooms.get(roomId);
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
//todo: this should cause the http call to return a 500
|
//todo: this should cause the http call to return a 500
|
||||||
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In sendAdminRoomMessage, could not find room with id '" +
|
||||||
|
roomId +
|
||||||
|
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
room.getUsers().forEach((recipient) => {
|
room.getUsers().forEach((recipient) => {
|
||||||
const sendUserMessage = new SendUserMessage();
|
const sendUserMessage = new SendUserMessage();
|
||||||
sendUserMessage.setMessage(message);
|
sendUserMessage.setMessage(message);
|
||||||
sendUserMessage.setType('message');
|
sendUserMessage.setType(type);
|
||||||
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
const clientMessage = new ServerToClientMessage();
|
||||||
clientMessage.setSendusermessage(sendUserMessage);
|
clientMessage.setSendusermessage(sendUserMessage);
|
||||||
@ -715,14 +785,18 @@ export class SocketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchWorlFullWarning(roomId: string,): void {
|
async dispatchWorldFullWarning(roomId: string): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
//todo: this should cause the http call to return a 500
|
//todo: this should cause the http call to return a 500
|
||||||
console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?");
|
console.error(
|
||||||
|
"In dispatchWorldFullWarning, could not find room with id '" +
|
||||||
|
roomId +
|
||||||
|
"'. Maybe the room was closed a few milliseconds ago and there was a race condition?"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
room.getUsers().forEach((recipient) => {
|
room.getUsers().forEach((recipient) => {
|
||||||
const worldFullMessage = new WorldFullWarningMessage();
|
const worldFullMessage = new WorldFullWarningMessage();
|
||||||
|
|
||||||
@ -733,17 +807,17 @@ export class SocketManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchRoomRefresh(roomId: string,): void {
|
async dispatchRoomRefresh(roomId: string): Promise<void> {
|
||||||
const room = this.rooms.get(roomId);
|
const room = await this.roomsPromises.get(roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionNumber = room.incrementVersion();
|
const versionNumber = room.incrementVersion();
|
||||||
room.getUsers().forEach((recipient) => {
|
room.getUsers().forEach((recipient) => {
|
||||||
const worldFullMessage = new RefreshRoomMessage();
|
const worldFullMessage = new RefreshRoomMessage();
|
||||||
worldFullMessage.setRoomid(roomId)
|
worldFullMessage.setRoomid(roomId);
|
||||||
worldFullMessage.setVersionnumber(versionNumber)
|
worldFullMessage.setVersionnumber(versionNumber);
|
||||||
|
|
||||||
const clientMessage = new ServerToClientMessage();
|
const clientMessage = new ServerToClientMessage();
|
||||||
clientMessage.setRefreshroommessage(worldFullMessage);
|
clientMessage.setRefreshroommessage(worldFullMessage);
|
||||||
@ -751,6 +825,13 @@ export class SocketManager {
|
|||||||
recipient.socket.write(clientMessage);
|
recipient.socket.write(clientMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEmoteEventMessage(room: GameRoom, user: User, emotePromptMessage: EmotePromptMessage) {
|
||||||
|
const emoteEventMessage = new EmoteEventMessage();
|
||||||
|
emoteEventMessage.setEmote(emotePromptMessage.getEmote());
|
||||||
|
emoteEventMessage.setActoruserid(user.id);
|
||||||
|
room.emitEmoteEvent(user, emoteEventMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const socketManager = new SocketManager();
|
export const socketManager = new SocketManager();
|
||||||
|
9
back/src/Services/VariableError.ts
Normal file
9
back/src/Services/VariableError.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Errors related to variable handling.
|
||||||
|
*/
|
||||||
|
export class VariableError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, VariableError.prototype);
|
||||||
|
}
|
||||||
|
}
|
234
back/src/Services/VariablesManager.ts
Normal file
234
back/src/Services/VariablesManager.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
/**
|
||||||
|
* Handles variables shared between the scripting API and the server.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
ITiledMap,
|
||||||
|
ITiledMapLayer,
|
||||||
|
ITiledMapObject,
|
||||||
|
ITiledMapObjectLayer,
|
||||||
|
} from "@workadventure/tiled-map-type-guard/dist";
|
||||||
|
import { User } from "_Model/User";
|
||||||
|
import { variablesRepository } from "./Repository/VariablesRepository";
|
||||||
|
import { redisClient } from "./RedisClient";
|
||||||
|
import { VariableError } from "./VariableError";
|
||||||
|
|
||||||
|
interface Variable {
|
||||||
|
defaultValue?: string;
|
||||||
|
persist?: boolean;
|
||||||
|
readableBy?: string;
|
||||||
|
writableBy?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VariablesManager {
|
||||||
|
/**
|
||||||
|
* The actual values of the variables for the current room
|
||||||
|
*/
|
||||||
|
private _variables = new Map<string, string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of variables that are allowed
|
||||||
|
*/
|
||||||
|
private variableObjects: Map<string, Variable> | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param map The map can be "null" if it is hosted on a private network. In this case, we assume this is a test setup and bypass any server-side checks.
|
||||||
|
*/
|
||||||
|
constructor(private roomUrl: string, private map: ITiledMap | null) {
|
||||||
|
// We initialize the list of variable object at room start. The objects cannot be edited later
|
||||||
|
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
|
||||||
|
if (map) {
|
||||||
|
this.variableObjects = VariablesManager.findVariablesInMap(map);
|
||||||
|
|
||||||
|
// Let's initialize default values
|
||||||
|
for (const [name, variableObject] of this.variableObjects.entries()) {
|
||||||
|
if (variableObject.defaultValue !== undefined) {
|
||||||
|
this._variables.set(name, variableObject.defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let's load data from the Redis backend.
|
||||||
|
*/
|
||||||
|
public async init(): Promise<VariablesManager> {
|
||||||
|
if (!this.shouldPersist()) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
const variables = await variablesRepository.loadVariables(this.roomUrl);
|
||||||
|
for (const key in variables) {
|
||||||
|
// Let's only set variables if they are in the map (if the map has changed, maybe stored variables do not exist anymore)
|
||||||
|
if (this.variableObjects) {
|
||||||
|
const variableObject = this.variableObjects.get(key);
|
||||||
|
if (variableObject === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!variableObject.persist) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._variables.set(key, variables[key]);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if saving should be enabled, and false otherwise.
|
||||||
|
*
|
||||||
|
* Saving is enabled if REDIS_HOST is set
|
||||||
|
* unless we are editing a local map
|
||||||
|
* unless we are in dev mode in which case it is ok to save
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private shouldPersist(): boolean {
|
||||||
|
return redisClient !== null && (this.map !== null || process.env.NODE_ENV === "development");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static findVariablesInMap(map: ITiledMap): Map<string, Variable> {
|
||||||
|
const objects = new Map<string, Variable>();
|
||||||
|
for (const layer of map.layers) {
|
||||||
|
this.recursiveFindVariablesInLayer(layer, objects);
|
||||||
|
}
|
||||||
|
return objects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static recursiveFindVariablesInLayer(layer: ITiledMapLayer, objects: Map<string, Variable>): void {
|
||||||
|
if (layer.type === "objectgroup") {
|
||||||
|
for (const object of layer.objects) {
|
||||||
|
if (object.type === "variable") {
|
||||||
|
if (object.template) {
|
||||||
|
console.warn(
|
||||||
|
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We store a copy of the object (to make it immutable)
|
||||||
|
objects.set(object.name, this.iTiledObjectToVariable(object));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (layer.type === "group") {
|
||||||
|
for (const innerLayer of layer.layers) {
|
||||||
|
this.recursiveFindVariablesInLayer(innerLayer, objects);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
|
||||||
|
const variable: Variable = {};
|
||||||
|
|
||||||
|
if (object.properties) {
|
||||||
|
for (const property of object.properties) {
|
||||||
|
const value = property.value;
|
||||||
|
switch (property.name) {
|
||||||
|
case "default":
|
||||||
|
variable.defaultValue = JSON.stringify(value);
|
||||||
|
break;
|
||||||
|
case "persist":
|
||||||
|
if (typeof value !== "boolean") {
|
||||||
|
throw new Error('The persist property of variable "' + object.name + '" must be a boolean');
|
||||||
|
}
|
||||||
|
variable.persist = value;
|
||||||
|
break;
|
||||||
|
case "writableBy":
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(
|
||||||
|
'The writableBy property of variable "' + object.name + '" must be a string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
variable.writableBy = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "readableBy":
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
throw new Error(
|
||||||
|
'The readableBy property of variable "' + object.name + '" must be a string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
variable.readableBy = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the variable.
|
||||||
|
*
|
||||||
|
* Returns who is allowed to read the variable (the readableby property) or "undefined" if anyone can read it.
|
||||||
|
* Also, returns "false" if the variable was not modified (because we set it to the value it already has)
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @param value
|
||||||
|
* @param user
|
||||||
|
*/
|
||||||
|
setVariable(name: string, value: string, user: User): string | undefined | false {
|
||||||
|
let readableBy: string | undefined;
|
||||||
|
let variableObject: Variable | undefined;
|
||||||
|
if (this.variableObjects) {
|
||||||
|
variableObject = this.variableObjects.get(name);
|
||||||
|
if (variableObject === undefined) {
|
||||||
|
throw new VariableError(
|
||||||
|
'Trying to set a variable "' + name + '" that is not defined as an object in the map.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variableObject.writableBy && !user.tags.includes(variableObject.writableBy)) {
|
||||||
|
throw new VariableError(
|
||||||
|
'Trying to set a variable "' +
|
||||||
|
name +
|
||||||
|
'". User "' +
|
||||||
|
user.name +
|
||||||
|
'" does not have sufficient permission. Required tag: "' +
|
||||||
|
variableObject.writableBy +
|
||||||
|
'". User tags: ' +
|
||||||
|
user.tags.join(", ") +
|
||||||
|
"."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
readableBy = variableObject.readableBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the value is not modified, return false
|
||||||
|
if (this._variables.get(name) === value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._variables.set(name, value);
|
||||||
|
|
||||||
|
if (variableObject !== undefined && variableObject.persist) {
|
||||||
|
variablesRepository
|
||||||
|
.saveVariable(this.roomUrl, name, value)
|
||||||
|
.catch((e) => console.error("Error while saving variable in Redis:", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
return readableBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getVariablesForTags(tags: string[]): Map<string, string> {
|
||||||
|
if (this.variableObjects === undefined) {
|
||||||
|
return this._variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
const readableVariables = new Map<string, string>();
|
||||||
|
|
||||||
|
for (const [key, value] of this._variables.entries()) {
|
||||||
|
const variableObject = this.variableObjects.get(key);
|
||||||
|
if (variableObject === undefined) {
|
||||||
|
throw new Error('Unexpected variable "' + key + '" found has no associated variableObject.');
|
||||||
|
}
|
||||||
|
if (!variableObject.readableBy || tags.includes(variableObject.readableBy)) {
|
||||||
|
readableVariables.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return readableVariables;
|
||||||
|
}
|
||||||
|
}
|
@ -1,55 +1,62 @@
|
|||||||
import "jasmine";
|
import "jasmine";
|
||||||
import {ConnectCallback, DisconnectCallback, GameRoom} from "../src/Model/GameRoom";
|
import { ConnectCallback, DisconnectCallback, GameRoom } from "../src/Model/GameRoom";
|
||||||
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
import { Point } from "../src/Model/Websocket/MessageUserPosition";
|
||||||
import {Group} from "../src/Model/Group";
|
import { Group } from "../src/Model/Group";
|
||||||
import {User, UserSocket} from "_Model/User";
|
import { User, UserSocket } from "_Model/User";
|
||||||
import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb";
|
import { JoinRoomMessage, PositionMessage } from "../src/Messages/generated/messages_pb";
|
||||||
import Direction = PositionMessage.Direction;
|
import Direction = PositionMessage.Direction;
|
||||||
|
import { EmoteCallback } from "_Model/Zone";
|
||||||
|
|
||||||
function createMockUser(userId: number): User {
|
function createMockUser(userId: number): User {
|
||||||
return {
|
return {
|
||||||
userId
|
userId,
|
||||||
} as unknown as User;
|
} as unknown as User;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMockUserSocket(): UserSocket {
|
function createMockUserSocket(): UserSocket {
|
||||||
return {
|
return {} as unknown as UserSocket;
|
||||||
} as unknown as UserSocket;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage
|
function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMessage {
|
||||||
{
|
|
||||||
const positionMessage = new PositionMessage();
|
const positionMessage = new PositionMessage();
|
||||||
positionMessage.setX(x);
|
positionMessage.setX(x);
|
||||||
positionMessage.setY(y);
|
positionMessage.setY(y);
|
||||||
positionMessage.setDirection(Direction.DOWN);
|
positionMessage.setDirection(Direction.DOWN);
|
||||||
positionMessage.setMoving(false);
|
positionMessage.setMoving(false);
|
||||||
const joinRoomMessage = new JoinRoomMessage();
|
const joinRoomMessage = new JoinRoomMessage();
|
||||||
joinRoomMessage.setUseruuid('1');
|
joinRoomMessage.setUseruuid("1");
|
||||||
joinRoomMessage.setIpaddress('10.0.0.2');
|
joinRoomMessage.setIpaddress("10.0.0.2");
|
||||||
joinRoomMessage.setName('foo');
|
joinRoomMessage.setName("foo");
|
||||||
joinRoomMessage.setRoomid('_/global/test.json');
|
joinRoomMessage.setRoomid("_/global/test.json");
|
||||||
joinRoomMessage.setPositionmessage(positionMessage);
|
joinRoomMessage.setPositionmessage(positionMessage);
|
||||||
return joinRoomMessage;
|
return joinRoomMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emote: EmoteCallback = (emoteEventMessage, listener): void => {};
|
||||||
|
|
||||||
describe("GameRoom", () => {
|
describe("GameRoom", () => {
|
||||||
it("should connect user1 and user2", () => {
|
it("should connect user1 and user2", async () => {
|
||||||
let connectCalledNumber: number = 0;
|
let connectCalledNumber: number = 0;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
connectCalledNumber++;
|
connectCalledNumber++;
|
||||||
}
|
};
|
||||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
||||||
|
|
||||||
}
|
const world = await GameRoom.create(
|
||||||
|
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
160,
|
||||||
|
160,
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
emote
|
||||||
|
);
|
||||||
|
|
||||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||||
|
|
||||||
|
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 500, 100));
|
||||||
|
|
||||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
|
||||||
|
|
||||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 500, 100));
|
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(261, 100));
|
world.updatePosition(user2, new Point(261, 100));
|
||||||
|
|
||||||
@ -63,26 +70,34 @@ describe("GameRoom", () => {
|
|||||||
expect(connectCalledNumber).toBe(2);
|
expect(connectCalledNumber).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should connect 3 users", () => {
|
it("should connect 3 users", async () => {
|
||||||
let connectCalled: boolean = false;
|
let connectCalled: boolean = false;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
connectCalled = true;
|
connectCalled = true;
|
||||||
}
|
};
|
||||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
const disconnect: DisconnectCallback = (user: User, group: Group): void => {};
|
||||||
|
|
||||||
}
|
const world = await GameRoom.create(
|
||||||
|
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
160,
|
||||||
|
160,
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
emote
|
||||||
|
);
|
||||||
|
|
||||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||||
|
|
||||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 200, 100));
|
||||||
|
|
||||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 200, 100));
|
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
expect(connectCalled).toBe(true);
|
||||||
connectCalled = false;
|
connectCalled = false;
|
||||||
|
|
||||||
// baz joins at the outer limit of the group
|
// baz joins at the outer limit of the group
|
||||||
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 311, 100));
|
const user3 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 311, 100));
|
||||||
|
|
||||||
expect(connectCalled).toBe(false);
|
expect(connectCalled).toBe(false);
|
||||||
|
|
||||||
@ -91,31 +106,40 @@ describe("GameRoom", () => {
|
|||||||
expect(connectCalled).toBe(true);
|
expect(connectCalled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should disconnect user1 and user2", () => {
|
it("should disconnect user1 and user2", async () => {
|
||||||
let connectCalled: boolean = false;
|
let connectCalled: boolean = false;
|
||||||
let disconnectCallNumber: number = 0;
|
let disconnectCallNumber: number = 0;
|
||||||
const connect: ConnectCallback = (user: User, group: Group): void => {
|
const connect: ConnectCallback = (user: User, group: Group): void => {
|
||||||
connectCalled = true;
|
connectCalled = true;
|
||||||
}
|
};
|
||||||
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
const disconnect: DisconnectCallback = (user: User, group: Group): void => {
|
||||||
disconnectCallNumber++;
|
disconnectCallNumber++;
|
||||||
}
|
};
|
||||||
|
|
||||||
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
|
const world = await GameRoom.create(
|
||||||
|
"https://play.workadventu.re/_/global/localhost/test.json",
|
||||||
|
connect,
|
||||||
|
disconnect,
|
||||||
|
160,
|
||||||
|
160,
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
emote
|
||||||
|
);
|
||||||
|
|
||||||
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
|
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage("1", 100, 100));
|
||||||
|
|
||||||
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage('2', 259, 100));
|
const user2 = world.join(createMockUserSocket(), createJoinRoomMessage("2", 259, 100));
|
||||||
|
|
||||||
expect(connectCalled).toBe(true);
|
expect(connectCalled).toBe(true);
|
||||||
expect(disconnectCallNumber).toBe(0);
|
expect(disconnectCallNumber).toBe(0);
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(100+160+160+1, 100));
|
world.updatePosition(user2, new Point(100 + 160 + 160 + 1, 100));
|
||||||
|
|
||||||
expect(disconnectCallNumber).toBe(2);
|
expect(disconnectCallNumber).toBe(2);
|
||||||
|
|
||||||
world.updatePosition(user2, new Point(262, 100));
|
world.updatePosition(user2, new Point(262, 100));
|
||||||
expect(disconnectCallNumber).toBe(2);
|
expect(disconnectCallNumber).toBe(2);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
})
|
|
||||||
|
32
back/tests/MapFetcherTest.ts
Normal file
32
back/tests/MapFetcherTest.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { arrayIntersect } from "../src/Services/ArrayHelper";
|
||||||
|
import { mapFetcher } from "../src/Services/MapFetcher";
|
||||||
|
|
||||||
|
describe("MapFetcher", () => {
|
||||||
|
it("should return true on localhost ending URLs", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://localhost")).toBeTrue();
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://foo.localhost")).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true on DNS resolving to a local domain", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://127.0.0.1.nip.io")).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true on an IP resolving to a local domain", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://127.0.0.1")).toBeTrue();
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://192.168.0.1")).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false on an IP resolving to a global domain", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://51.12.42.42")).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false on an DNS resolving to a global domain", async () => {
|
||||||
|
expect(await mapFetcher.isLocalUrl("https://maps.workadventu.re")).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error on invalid domain", async () => {
|
||||||
|
await expectAsync(
|
||||||
|
mapFetcher.isLocalUrl("https://this.domain.name.doesnotexistfoobgjkgfdjkgldf.com")
|
||||||
|
).toBeRejected();
|
||||||
|
});
|
||||||
|
});
|
@ -1,10 +1,6 @@
|
|||||||
import "jasmine";
|
import "jasmine";
|
||||||
import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom";
|
|
||||||
import {Point} from "../src/Model/Websocket/MessageUserPosition";
|
|
||||||
import { Group } from "../src/Model/Group";
|
|
||||||
import {PositionNotifier} from "../src/Model/PositionNotifier";
|
import {PositionNotifier} from "../src/Model/PositionNotifier";
|
||||||
import {User, UserSocket} from "../src/Model/User";
|
import {User, UserSocket} from "../src/Model/User";
|
||||||
import {PointInterface} from "../src/Model/Websocket/PointInterface";
|
|
||||||
import {Zone} from "_Model/Zone";
|
import {Zone} from "_Model/Zone";
|
||||||
import {Movable} from "_Model/Movable";
|
import {Movable} from "_Model/Movable";
|
||||||
import {PositionInterface} from "_Model/PositionInterface";
|
import {PositionInterface} from "_Model/PositionInterface";
|
||||||
@ -23,21 +19,21 @@ describe("PositionNotifier", () => {
|
|||||||
moveTriggered = true;
|
moveTriggered = true;
|
||||||
}, (thing: Movable) => {
|
}, (thing: Movable) => {
|
||||||
leaveTriggered = true;
|
leaveTriggered = true;
|
||||||
});
|
}, () => {});
|
||||||
|
|
||||||
const user1 = new User(1, 'test', '10.0.0.2', {
|
const user1 = new User(1, 'test', '10.0.0.2', {
|
||||||
x: 500,
|
x: 500,
|
||||||
y: 500,
|
y: 500,
|
||||||
moving: false,
|
moving: false,
|
||||||
direction: 'down'
|
direction: 'down'
|
||||||
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
|
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
|
||||||
|
|
||||||
const user2 = new User(2, 'test', '10.0.0.2', {
|
const user2 = new User(2, 'test', '10.0.0.2', {
|
||||||
x: -9999,
|
x: -9999,
|
||||||
y: -9999,
|
y: -9999,
|
||||||
moving: false,
|
moving: false,
|
||||||
direction: 'down'
|
direction: 'down'
|
||||||
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
|
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
|
||||||
|
|
||||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
|
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
|
||||||
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
|
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
|
||||||
@ -98,21 +94,21 @@ describe("PositionNotifier", () => {
|
|||||||
moveTriggered = true;
|
moveTriggered = true;
|
||||||
}, (thing: Movable) => {
|
}, (thing: Movable) => {
|
||||||
leaveTriggered = true;
|
leaveTriggered = true;
|
||||||
});
|
}, () => {});
|
||||||
|
|
||||||
const user1 = new User(1, 'test', '10.0.0.2', {
|
const user1 = new User(1, 'test', '10.0.0.2', {
|
||||||
x: 500,
|
x: 500,
|
||||||
y: 500,
|
y: 500,
|
||||||
moving: false,
|
moving: false,
|
||||||
direction: 'down'
|
direction: 'down'
|
||||||
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
|
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
|
||||||
|
|
||||||
const user2 = new User(2, 'test', '10.0.0.2', {
|
const user2 = new User(2, 'test', '10.0.0.2', {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
moving: false,
|
moving: false,
|
||||||
direction: 'down'
|
direction: 'down'
|
||||||
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
|
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
|
||||||
|
|
||||||
const listener = {} as ZoneSocket;
|
const listener = {} as ZoneSocket;
|
||||||
positionNotifier.addZoneListener(listener, 0, 0);
|
positionNotifier.addZoneListener(listener, 0, 0);
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "../src/Model/RoomIdentifier";
|
|
||||||
|
|
||||||
describe("RoomIdentifier", () => {
|
|
||||||
it("should flag public id as anonymous", () => {
|
|
||||||
expect(isRoomAnonymous('_/global/test')).toBe(true);
|
|
||||||
});
|
|
||||||
it("should flag public id as not anonymous", () => {
|
|
||||||
expect(isRoomAnonymous('@/afup/afup2020/1floor')).toBe(false);
|
|
||||||
});
|
|
||||||
it("should extract roomSlug from public ID", () => {
|
|
||||||
expect(extractRoomSlugPublicRoomId('_/global/npeguin/test.json')).toBe('npeguin/test.json');
|
|
||||||
});
|
|
||||||
it("should extract correct from private ID", () => {
|
|
||||||
const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId('@/afup/afup2020/1floor');
|
|
||||||
expect(organizationSlug).toBe('afup');
|
|
||||||
expect(worldSlug).toBe('afup2020');
|
|
||||||
expect(roomSlug).toBe('1floor');
|
|
||||||
});
|
|
||||||
})
|
|
67
back/tests/getNearbyDescriptorsMatrixTest.ts
Normal file
67
back/tests/getNearbyDescriptorsMatrixTest.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import "jasmine";
|
||||||
|
import { getNearbyDescriptorsMatrix } from "../src/Model/PositionNotifier";
|
||||||
|
|
||||||
|
describe("getNearbyDescriptorsMatrix", () => {
|
||||||
|
it("should create a matrix of coordinates in a square around the parameter", () => {
|
||||||
|
const matrix = [];
|
||||||
|
for (const d of getNearbyDescriptorsMatrix({ i: 1, j: 1 })) {
|
||||||
|
matrix.push(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(matrix).toEqual([
|
||||||
|
{ i: 0, j: 0 },
|
||||||
|
{ i: 1, j: 0 },
|
||||||
|
{ i: 2, j: 0 },
|
||||||
|
{ i: 0, j: 1 },
|
||||||
|
{ i: 1, j: 1 },
|
||||||
|
{ i: 2, j: 1 },
|
||||||
|
{ i: 0, j: 2 },
|
||||||
|
{ i: 1, j: 2 },
|
||||||
|
{ i: 2, j: 2 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a matrix of coordinates in a square around the parameter bis", () => {
|
||||||
|
const matrix = [];
|
||||||
|
for (const d of getNearbyDescriptorsMatrix({ i: 8, j: 3 })) {
|
||||||
|
matrix.push(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(matrix).toEqual([
|
||||||
|
{ i: 7, j: 2 },
|
||||||
|
{ i: 8, j: 2 },
|
||||||
|
{ i: 9, j: 2 },
|
||||||
|
{ i: 7, j: 3 },
|
||||||
|
{ i: 8, j: 3 },
|
||||||
|
{ i: 9, j: 3 },
|
||||||
|
{ i: 7, j: 4 },
|
||||||
|
{ i: 8, j: 4 },
|
||||||
|
{ i: 9, j: 4 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create a matrix with negative coordinates", () => {
|
||||||
|
const matrix = [];
|
||||||
|
for (const d of getNearbyDescriptorsMatrix({ i: 0, j: 0 })) {
|
||||||
|
matrix.push(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(matrix).toEqual([
|
||||||
|
{ i: 0, j: 0 },
|
||||||
|
{ i: 1, j: 0 },
|
||||||
|
{ i: 0, j: 1 },
|
||||||
|
{ i: 1, j: 1 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*it("should not create a matrix with coordinates bigger than its dimmensions", () => {
|
||||||
|
const matrix = getNearbyDescriptorsMatrix({i: 4, j: 4}, 5, 5);
|
||||||
|
|
||||||
|
expect(matrix).toEqual([
|
||||||
|
{i: 3,j: 3},
|
||||||
|
{i: 4,j: 3},
|
||||||
|
{i: 3,j: 4},
|
||||||
|
{i: 4,j: 4},
|
||||||
|
])
|
||||||
|
});*/
|
||||||
|
});
|
@ -3,7 +3,7 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
"target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
734
back/yarn.lock
734
back/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,6 @@ RoomConnection.setWebsocketFactory((url: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function startOneUser(): Promise<void> {
|
async function startOneUser(): Promise<void> {
|
||||||
await connectionManager.anonymousLogin(true);
|
|
||||||
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
||||||
{
|
{
|
||||||
x: 783,
|
x: 783,
|
||||||
@ -23,7 +22,7 @@ async function startOneUser(): Promise<void> {
|
|||||||
bottom: 200,
|
bottom: 200,
|
||||||
left: 500,
|
left: 500,
|
||||||
right: 800
|
right: 800
|
||||||
});
|
}, null);
|
||||||
|
|
||||||
const connection = onConnect.connection;
|
const connection = onConnect.connection;
|
||||||
|
|
||||||
|
24
benchmark/package-lock.json
generated
24
benchmark/package-lock.json
generated
@ -209,9 +209,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"glob-parent": {
|
"glob-parent": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
}
|
}
|
||||||
@ -230,9 +230,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hosted-git-info": {
|
"hosted-git-info": {
|
||||||
"version": "2.8.8",
|
"version": "2.8.9",
|
||||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
|
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
|
||||||
},
|
},
|
||||||
"indent-string": {
|
"indent-string": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
@ -429,9 +429,9 @@
|
|||||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||||
},
|
},
|
||||||
"path-parse": {
|
"path-parse": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
|
||||||
},
|
},
|
||||||
"path-type": {
|
"path-type": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@ -688,9 +688,9 @@
|
|||||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||||
},
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.3.1",
|
"version": "7.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
|
||||||
"integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA=="
|
"integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A=="
|
||||||
},
|
},
|
||||||
"xtend": {
|
"xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
"@types/ws": "^7.2.6",
|
"@types/ws": "^7.2.6",
|
||||||
"ts-node-dev": "^1.0.0-pre.62",
|
"ts-node-dev": "^1.0.0-pre.62",
|
||||||
"typescript": "^4.0.2",
|
"typescript": "^4.0.2",
|
||||||
"ws": "^7.3.1"
|
"ws": "^7.4.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
}
|
}
|
||||||
|
@ -148,8 +148,8 @@ get-stdin@^4.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||||
|
|
||||||
glob-parent@~5.1.0:
|
glob-parent@~5.1.0:
|
||||||
version "5.1.1"
|
version "5.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
|
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob "^4.0.1"
|
is-glob "^4.0.1"
|
||||||
|
|
||||||
@ -169,8 +169,8 @@ graceful-fs@^4.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
|
||||||
|
|
||||||
hosted-git-info@^2.1.4:
|
hosted-git-info@^2.1.4:
|
||||||
version "2.8.8"
|
version "2.8.9"
|
||||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||||
|
|
||||||
indent-string@^2.1.0:
|
indent-string@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
@ -315,8 +315,8 @@ path-is-absolute@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||||
|
|
||||||
path-parse@^1.0.6:
|
path-parse@^1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
|
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||||
|
|
||||||
path-type@^1.0.0:
|
path-type@^1.0.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
@ -515,9 +515,9 @@ wrappy@1:
|
|||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||||
|
|
||||||
ws@^7.3.1:
|
ws@^7.4.6:
|
||||||
version "7.3.1"
|
version "7.4.6"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
|
||||||
|
|
||||||
xtend@^4.0.0:
|
xtend@^4.0.0:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
|
@ -38,6 +38,7 @@ services:
|
|||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
|
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
|
||||||
PUSHER_URL: //pusher.${DOMAIN}
|
PUSHER_URL: //pusher.${DOMAIN}
|
||||||
|
ICON_URL: //icon.${DOMAIN}
|
||||||
TURN_SERVER: "${TURN_SERVER}"
|
TURN_SERVER: "${TURN_SERVER}"
|
||||||
TURN_USER: "${TURN_USER}"
|
TURN_USER: "${TURN_USER}"
|
||||||
TURN_PASSWORD: "${TURN_PASSWORD}"
|
TURN_PASSWORD: "${TURN_PASSWORD}"
|
||||||
@ -65,6 +66,7 @@ services:
|
|||||||
API_URL: back:50051
|
API_URL: back:50051
|
||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
|
FRONT_URL: https://play.${DOMAIN}
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)"
|
- "traefik.http.routers.pusher.rule=Host(`pusher.${DOMAIN}`)"
|
||||||
- "traefik.http.routers.pusher.entryPoints=web,traefik"
|
- "traefik.http.routers.pusher.entryPoints=web,traefik"
|
||||||
@ -98,3 +100,15 @@ services:
|
|||||||
- "traefik.http.routers.back-ssl.service=back"
|
- "traefik.http.routers.back-ssl.service=back"
|
||||||
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
|
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
icon:
|
||||||
|
image: matthiasluedtke/iconserver:v3.13.0
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.icon.rule=Host(`icon.${DOMAIN}`)"
|
||||||
|
- "traefik.http.routers.icon.entryPoints=web,traefik"
|
||||||
|
- "traefik.http.services.icon.loadbalancer.server.port=8080"
|
||||||
|
- "traefik.http.routers.icon-ssl.rule=Host(`icon.${DOMAIN}`)"
|
||||||
|
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.icon-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.icon-ssl.service=icon"
|
||||||
|
- "traefik.http.routers.icon-ssl.tls.certresolver=myresolver"
|
||||||
|
19
debian/changelog
vendored
19
debian/changelog
vendored
@ -1,3 +1,22 @@
|
|||||||
|
workadventure (1.6.3-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Update to 1.6.3
|
||||||
|
|
||||||
|
-- unknown <felix@localhost> Mon, 29 Nov 2021 20:10:29 +0200
|
||||||
|
|
||||||
|
workadventure (1.4.12-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Update to 1.4.12
|
||||||
|
|
||||||
|
-- unknown <felix@localhost> Fri, 06 Aug 2021 15:00:35 +0200
|
||||||
|
|
||||||
|
workadventure (1.2.4-2) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Fix chat dialog spacing
|
||||||
|
* Improve systemd unit for backend
|
||||||
|
|
||||||
|
-- unknown <felix@localhost> Wed, 21 Apr 2021 21:55:41 +0200
|
||||||
|
|
||||||
workadventure (1.2.4-1) unstable; urgency=medium
|
workadventure (1.2.4-1) unstable; urgency=medium
|
||||||
|
|
||||||
* Initial release
|
* Initial release
|
||||||
|
4
debian/rules
vendored
4
debian/rules
vendored
@ -5,12 +5,12 @@
|
|||||||
override_dh_install:
|
override_dh_install:
|
||||||
install -Dt debian/workadventure-front/usr/src/workadventure/front front/dist/index.html
|
install -Dt debian/workadventure-front/usr/src/workadventure/front front/dist/index.html
|
||||||
install -d debian/workadventure-front/usr/src/workadventure/front/js
|
install -d debian/workadventure-front/usr/src/workadventure/front/js
|
||||||
cp -r front/dist/static front/dist/resources front/dist/*.js front/dist/style*.css debian/workadventure-front/usr/src/workadventure/front
|
cp -r front/dist/static front/dist/fonts front/dist/resources front/dist/*.js front/dist/*.png front/dist/*.css debian/workadventure-front/usr/src/workadventure/front
|
||||||
cp -r front/dist/js/*.js debian/workadventure-front/usr/src/workadventure/front/js
|
cp -r front/dist/js/*.js debian/workadventure-front/usr/src/workadventure/front/js
|
||||||
|
|
||||||
|
|
||||||
install -d debian/workadventure-front-map/usr/src/workadventure/front/js
|
install -d debian/workadventure-front-map/usr/src/workadventure/front/js
|
||||||
cp -r front/dist/*.js.map front/dist/style*.css.map debian/workadventure-front-map/usr/src/workadventure/front
|
cp -r front/dist/*.js.map front/dist/*.css.map debian/workadventure-front-map/usr/src/workadventure/front
|
||||||
cp -r front/dist/js/*.js.map debian/workadventure-front-map/usr/src/workadventure/front/js
|
cp -r front/dist/js/*.js.map debian/workadventure-front-map/usr/src/workadventure/front/js
|
||||||
|
|
||||||
install -d debian/workadventure-back/usr/src/workadventure/back
|
install -d debian/workadventure-back/usr/src/workadventure/back
|
||||||
|
2
debian/workadventure-back.service
vendored
2
debian/workadventure-back.service
vendored
@ -6,6 +6,8 @@ User=webspace-user
|
|||||||
WorkingDirectory=/var/lib/workadventure/back
|
WorkingDirectory=/var/lib/workadventure/back
|
||||||
Environment=NODE_PATH=node_modules
|
Environment=NODE_PATH=node_modules
|
||||||
ExecStart=node --max-old-space-size=4096 server.js
|
ExecStart=node --max-old-space-size=4096 server.js
|
||||||
|
ProtectSystem=Full
|
||||||
|
ProtectHome=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
@ -1,103 +1,95 @@
|
|||||||
{
|
{
|
||||||
local env = std.extVar("env"),
|
local env = std.extVar("env"),
|
||||||
local namespace = env.GITHUB_REF_SLUG,
|
local namespace = env.DEPLOY_REF,
|
||||||
local tag = namespace,
|
local tag = namespace,
|
||||||
local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com",
|
local url = namespace+".test.workadventu.re",
|
||||||
// develop branch does not use admin because of issue with SSL certificate of admin as of now.
|
// develop branch does not use admin because of issue with SSL certificate of admin as of now.
|
||||||
local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://"+url else null,
|
local adminUrl = if std.startsWith(namespace, "admin") then "https://"+url else null,
|
||||||
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
|
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"containers": {
|
"containers": {
|
||||||
"back1": {
|
"back1": {
|
||||||
"image": "thecodingmachine/workadventure-back:"+tag,
|
"image": "thecodingmachine/workadventure-back:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "api1."+url,
|
"url": "api1-"+url,
|
||||||
"https": "enable",
|
|
||||||
"containerPort": 8080
|
"containerPort": 8080
|
||||||
},
|
},
|
||||||
"ports": [8080, 50051],
|
"ports": [8080, 50051],
|
||||||
"env": {
|
"env": {
|
||||||
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
||||||
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
|
||||||
"JITSI_ISS": env.JITSI_ISS,
|
"JITSI_ISS": env.JITSI_ISS,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
|
"REDIS_HOST": "redis",
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {}) + if namespace != "master" then {
|
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
||||||
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
|
} else {})
|
||||||
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"back2": {
|
"back2": {
|
||||||
"image": "thecodingmachine/workadventure-back:"+tag,
|
"image": "thecodingmachine/workadventure-back:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "api2."+url,
|
"url": "api2-"+url,
|
||||||
"https": "enable",
|
|
||||||
"containerPort": 8080
|
"containerPort": 8080
|
||||||
},
|
},
|
||||||
"ports": [8080, 50051],
|
"ports": [8080, 50051],
|
||||||
"env": {
|
"env": {
|
||||||
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
||||||
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
|
||||||
"JITSI_ISS": env.JITSI_ISS,
|
"JITSI_ISS": env.JITSI_ISS,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
|
||||||
|
"REDIS_HOST": "redis",
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {}) + if namespace != "master" then {
|
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
||||||
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
|
} else {})
|
||||||
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"pusher": {
|
"pusher": {
|
||||||
"replicas": 2,
|
"replicas": 2,
|
||||||
"image": "thecodingmachine/workadventure-pusher:"+tag,
|
"image": "thecodingmachine/workadventure-pusher:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "pusher."+url,
|
"url": "pusher-"+url,
|
||||||
"https": "enable"
|
|
||||||
},
|
},
|
||||||
"ports": [8080],
|
"ports": [8080],
|
||||||
"env": {
|
"env": {
|
||||||
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
||||||
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
|
||||||
"JITSI_ISS": env.JITSI_ISS,
|
"JITSI_ISS": env.JITSI_ISS,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
"API_URL": "back1:50051,back2:50051",
|
"API_URL": "back1:50051,back2:50051",
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
|
"FRONT_URL": "https://play-"+url
|
||||||
} + (if adminUrl != null then {
|
} + (if adminUrl != null then {
|
||||||
"ADMIN_API_URL": adminUrl,
|
"ADMIN_API_URL": adminUrl,
|
||||||
} else {}) + if namespace != "master" then {
|
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
||||||
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
|
"ADMIN_SOCKETS_TOKEN": env.ADMIN_SOCKETS_TOKEN,
|
||||||
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
|
} else {})
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"front": {
|
"front": {
|
||||||
"image": "thecodingmachine/workadventure-front:"+tag,
|
"image": "thecodingmachine/workadventure-front:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "play."+url,
|
"url": "play-"+url,
|
||||||
"https": "enable"
|
|
||||||
},
|
},
|
||||||
"ports": [80],
|
"ports": [80],
|
||||||
"env": {
|
"env": {
|
||||||
"PUSHER_URL": "//pusher."+url,
|
"PUSHER_URL": "//pusher-"+url,
|
||||||
"UPLOADER_URL": "//uploader."+url,
|
"UPLOADER_URL": "//uploader-"+url,
|
||||||
"ADMIN_URL": "//"+url,
|
"ADMIN_URL": "//"+url,
|
||||||
"JITSI_URL": env.JITSI_URL,
|
"JITSI_URL": env.JITSI_URL,
|
||||||
|
#POSTHOG
|
||||||
|
"POSTHOG_API_KEY": if namespace == "master" then env.POSTHOG_API_KEY else "",
|
||||||
|
"POSTHOG_URL": if namespace == "master" then env.POSTHOG_URL else "",
|
||||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||||
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
|
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
|
||||||
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
|
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
|
||||||
"START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json"
|
"START_ROOM_URL": "/_/global/maps-"+url+"/starter/map.json"
|
||||||
//"GA_TRACKING_ID": "UA-10196481-11"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"uploader": {
|
"uploader": {
|
||||||
"image": "thecodingmachine/workadventure-uploader:"+tag,
|
"image": "thecodingmachine/workadventure-uploader:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "uploader."+url,
|
"url": "uploader-"+url,
|
||||||
"https": "enable",
|
|
||||||
"containerPort": 8080
|
"containerPort": 8080
|
||||||
},
|
},
|
||||||
"ports": [8080],
|
"ports": [8080],
|
||||||
@ -107,16 +99,16 @@
|
|||||||
"maps": {
|
"maps": {
|
||||||
"image": "thecodingmachine/workadventure-maps:"+tag,
|
"image": "thecodingmachine/workadventure-maps:"+tag,
|
||||||
"host": {
|
"host": {
|
||||||
"url": "maps."+url,
|
"url": "maps-"+url
|
||||||
"https": "enable"
|
|
||||||
},
|
},
|
||||||
"ports": [80]
|
"ports": [80]
|
||||||
},
|
},
|
||||||
|
"redis": {
|
||||||
|
"image": "redis:6",
|
||||||
|
"ports": [6379]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"https": {
|
|
||||||
"mail": "d.negrier@thecodingmachine.com"
|
|
||||||
},
|
|
||||||
k8sextension(k8sConf)::
|
k8sextension(k8sConf)::
|
||||||
k8sConf + {
|
k8sConf + {
|
||||||
back1+: {
|
back1+: {
|
||||||
@ -131,6 +123,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
ingress+: {
|
||||||
|
spec+: {
|
||||||
|
tls+: [{
|
||||||
|
hosts: ["api1-"+url],
|
||||||
|
secretName: "certificate-tls"
|
||||||
|
}]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
back2+: {
|
back2+: {
|
||||||
@ -145,6 +145,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
ingress+: {
|
||||||
|
spec+: {
|
||||||
|
tls+: [{
|
||||||
|
hosts: ["api2-"+url],
|
||||||
|
secretName: "certificate-tls"
|
||||||
|
}]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pusher+: {
|
pusher+: {
|
||||||
@ -159,8 +167,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
ingress+: {
|
||||||
|
spec+: {
|
||||||
|
tls+: [{
|
||||||
|
hosts: ["pusher-"+url],
|
||||||
|
secretName: "certificate-tls"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
front+: {
|
||||||
|
ingress+: {
|
||||||
|
spec+: {
|
||||||
|
tls+: [{
|
||||||
|
hosts: ["play-"+url],
|
||||||
|
secretName: "certificate-tls"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uploader+: {
|
||||||
|
ingress+: {
|
||||||
|
spec+: {
|
||||||
|
tls+: [{
|
||||||
|
hosts: ["uploader-"+url],
|
||||||
|
secretName: "certificate-tls"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maps+: {
|
||||||
|
ingress+: {
|
||||||
|
spec+: {
|
||||||
|
tls+: [{
|
||||||
|
hosts: ["maps-"+url],
|
||||||
|
secretName: "certificate-tls"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,15 +28,20 @@ services:
|
|||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
PUSHER_URL: /pusher
|
PUSHER_URL: /pusher
|
||||||
UPLOADER_URL: /uploader
|
UPLOADER_URL: /uploader
|
||||||
ADMIN_URL: /admin
|
#ADMIN_URL: /admin
|
||||||
MAPS_URL: /maps
|
MAPS_URL: /maps
|
||||||
STARTUP_COMMAND_1: yarn install
|
ICON_URL: /icon
|
||||||
|
STARTUP_COMMAND_1: ./templater.sh
|
||||||
|
STARTUP_COMMAND_2: yarn install
|
||||||
TURN_SERVER: "turn:localhost:3478,turns:localhost:5349"
|
TURN_SERVER: "turn:localhost:3478,turns:localhost:5349"
|
||||||
|
DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS"
|
||||||
|
SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS"
|
||||||
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
|
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
|
||||||
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
|
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
|
||||||
TURN_USER: ""
|
TURN_USER: ""
|
||||||
TURN_PASSWORD: ""
|
TURN_PASSWORD: ""
|
||||||
START_ROOM_URL: "$START_ROOM_URL"
|
START_ROOM_URL: "$START_ROOM_URL"
|
||||||
|
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
|
||||||
command: yarn run start
|
command: yarn run start
|
||||||
volumes:
|
volumes:
|
||||||
- ./front:/usr/src/app
|
- ./front:/usr/src/app
|
||||||
@ -50,19 +55,28 @@ services:
|
|||||||
- "traefik.http.routers.front-ssl.service=front"
|
- "traefik.http.routers.front-ssl.service=front"
|
||||||
|
|
||||||
pusher:
|
pusher:
|
||||||
image: thecodingmachine/nodejs:12
|
image: thecodingmachine/nodejs:14
|
||||||
command: yarn dev
|
command: yarn dev
|
||||||
#command: yarn run prod
|
#command: yarn run prod
|
||||||
#command: yarn run profile
|
#command: yarn run profile
|
||||||
environment:
|
environment:
|
||||||
DEBUG: "*"
|
DEBUG: "*"
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
|
# wait for files generated by "messages" container to exists
|
||||||
|
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
||||||
SECRET_KEY: yourSecretKey
|
SECRET_KEY: yourSecretKey
|
||||||
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
||||||
API_URL: back:50051
|
API_URL: back:50051
|
||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
|
FRONT_URL: http://localhost
|
||||||
|
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
||||||
|
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
||||||
|
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
||||||
|
OPID_CLIENT_REDIRECT_URL: $OPID_CLIENT_REDIRECT_URL
|
||||||
|
OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER
|
||||||
|
DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS
|
||||||
volumes:
|
volumes:
|
||||||
- ./pusher:/usr/src/app
|
- ./pusher:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
@ -110,6 +124,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DEBUG: "*"
|
DEBUG: "*"
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
|
# wait for files generated by "messages" container to exists
|
||||||
|
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
||||||
SECRET_KEY: yourSecretKey
|
SECRET_KEY: yourSecretKey
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
||||||
ALLOW_ARTILLERY: "true"
|
ALLOW_ARTILLERY: "true"
|
||||||
@ -117,6 +133,8 @@ services:
|
|||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
||||||
|
REDIS_HOST: redis
|
||||||
|
NODE_ENV: development
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
@ -152,23 +170,6 @@ services:
|
|||||||
- "traefik.http.routers.uploader-ssl.tls=true"
|
- "traefik.http.routers.uploader-ssl.tls=true"
|
||||||
- "traefik.http.routers.uploader-ssl.service=uploader"
|
- "traefik.http.routers.uploader-ssl.service=uploader"
|
||||||
|
|
||||||
website:
|
|
||||||
image: thecodingmachine/nodejs:12-apache
|
|
||||||
environment:
|
|
||||||
STARTUP_COMMAND_1: npm install
|
|
||||||
STARTUP_COMMAND_2: npm run watch &
|
|
||||||
APACHE_DOCUMENT_ROOT: dist/
|
|
||||||
volumes:
|
|
||||||
- ./website:/var/www/html
|
|
||||||
labels:
|
|
||||||
- "traefik.http.routers.website.rule=Host(`workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.website.entryPoints=web"
|
|
||||||
- "traefik.http.services.website.loadbalancer.server.port=80"
|
|
||||||
- "traefik.http.routers.website-ssl.rule=Host(`workadventure.localhost`)"
|
|
||||||
- "traefik.http.routers.website-ssl.entryPoints=websecure"
|
|
||||||
- "traefik.http.routers.website-ssl.tls=true"
|
|
||||||
- "traefik.http.routers.website-ssl.service=website"
|
|
||||||
|
|
||||||
messages:
|
messages:
|
||||||
#image: thecodingmachine/nodejs:14
|
#image: thecodingmachine/nodejs:14
|
||||||
image: thecodingmachine/workadventure-back-base:latest
|
image: thecodingmachine/workadventure-back-base:latest
|
||||||
@ -182,6 +183,23 @@ services:
|
|||||||
- ./front:/usr/src/front
|
- ./front:/usr/src/front
|
||||||
- ./pusher:/usr/src/pusher
|
- ./pusher:/usr/src/pusher
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6
|
||||||
|
|
||||||
|
icon:
|
||||||
|
image: matthiasluedtke/iconserver:v3.13.0
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.strip-icon-prefix.stripprefix.prefixes=/icon"
|
||||||
|
- "traefik.http.routers.icon.rule=PathPrefix(`/icon`)"
|
||||||
|
- "traefik.http.routers.icon.middlewares=strip-icon-prefix@docker"
|
||||||
|
- "traefik.http.routers.icon.entryPoints=web"
|
||||||
|
- "traefik.http.services.icon.loadbalancer.server.port=8080"
|
||||||
|
- "traefik.http.routers.icon-ssl.rule=PathPrefix(`/icon`)"
|
||||||
|
- "traefik.http.routers.icon-ssl.middlewares=strip-icon-prefix@docker"
|
||||||
|
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.icon-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.icon-ssl.service=icon"
|
||||||
|
|
||||||
# coturn:
|
# coturn:
|
||||||
# image: coturn/coturn:4.5.2
|
# image: coturn/coturn:4.5.2
|
||||||
# command:
|
# command:
|
||||||
|
12
docker-compose.testcafe.yml
Normal file
12
docker-compose.testcafe.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
testcafe:
|
||||||
|
image: testcafe/testcafe:1.17.1
|
||||||
|
working_dir: /tests
|
||||||
|
environment:
|
||||||
|
BROWSER: "chromium --use-fake-device-for-media-stream"
|
||||||
|
volumes:
|
||||||
|
- ./tests:/tests
|
||||||
|
- ./maps:/maps
|
||||||
|
# security_opt:
|
||||||
|
# - seccomp:unconfined
|
@ -17,6 +17,12 @@ services:
|
|||||||
- front
|
- front
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
aliases:
|
||||||
|
- 'play.workadventure.localhost'
|
||||||
|
- 'pusher.workadventure.localhost'
|
||||||
|
- 'maps.workadventure.localhost'
|
||||||
|
|
||||||
front:
|
front:
|
||||||
image: thecodingmachine/nodejs:14
|
image: thecodingmachine/nodejs:14
|
||||||
@ -28,11 +34,14 @@ services:
|
|||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
PUSHER_URL: //pusher.workadventure.localhost
|
PUSHER_URL: //pusher.workadventure.localhost
|
||||||
UPLOADER_URL: //uploader.workadventure.localhost
|
UPLOADER_URL: //uploader.workadventure.localhost
|
||||||
ADMIN_URL: //workadventure.localhost
|
#ADMIN_URL: //workadventure.localhost
|
||||||
|
ICON_URL: //icon.workadventure.localhost
|
||||||
STARTUP_COMMAND_1: ./templater.sh
|
STARTUP_COMMAND_1: ./templater.sh
|
||||||
STARTUP_COMMAND_2: yarn install
|
STARTUP_COMMAND_2: yarn install
|
||||||
STUN_SERVER: "stun:stun.l.google.com:19302"
|
STUN_SERVER: "stun:stun.l.google.com:19302"
|
||||||
TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349"
|
TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349"
|
||||||
|
DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS"
|
||||||
|
SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS"
|
||||||
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
|
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
|
||||||
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
|
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
|
||||||
TURN_USER: ""
|
TURN_USER: ""
|
||||||
@ -40,6 +49,8 @@ services:
|
|||||||
START_ROOM_URL: "$START_ROOM_URL"
|
START_ROOM_URL: "$START_ROOM_URL"
|
||||||
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
MAX_PER_GROUP: "$MAX_PER_GROUP"
|
||||||
MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH"
|
MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH"
|
||||||
|
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
|
||||||
|
OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER"
|
||||||
command: yarn run start
|
command: yarn run start
|
||||||
volumes:
|
volumes:
|
||||||
- ./front:/usr/src/app
|
- ./front:/usr/src/app
|
||||||
@ -53,17 +64,26 @@ services:
|
|||||||
- "traefik.http.routers.front-ssl.service=front"
|
- "traefik.http.routers.front-ssl.service=front"
|
||||||
|
|
||||||
pusher:
|
pusher:
|
||||||
image: thecodingmachine/nodejs:12
|
image: thecodingmachine/nodejs:14
|
||||||
command: yarn dev
|
command: yarn dev
|
||||||
environment:
|
environment:
|
||||||
DEBUG: "socket:*"
|
DEBUG: "socket:*"
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
|
# wait for files generated by "messages" container to exists
|
||||||
|
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
||||||
SECRET_KEY: yourSecretKey
|
SECRET_KEY: yourSecretKey
|
||||||
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
|
||||||
API_URL: back:50051
|
API_URL: back:50051
|
||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
|
FRONT_URL: http://play.workadventure.localhost
|
||||||
|
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
||||||
|
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
||||||
|
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
||||||
|
OPID_CLIENT_REDIRECT_URL: $OPID_CLIENT_REDIRECT_URL
|
||||||
|
OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER
|
||||||
|
DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS
|
||||||
volumes:
|
volumes:
|
||||||
- ./pusher:/usr/src/app
|
- ./pusher:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
@ -105,6 +125,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DEBUG: "*"
|
DEBUG: "*"
|
||||||
STARTUP_COMMAND_1: yarn install
|
STARTUP_COMMAND_1: yarn install
|
||||||
|
# wait for files generated by "messages" container to exists
|
||||||
|
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
|
||||||
SECRET_KEY: yourSecretKey
|
SECRET_KEY: yourSecretKey
|
||||||
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
|
||||||
ALLOW_ARTILLERY: "true"
|
ALLOW_ARTILLERY: "true"
|
||||||
@ -113,6 +135,9 @@ services:
|
|||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
TURN_STATIC_AUTH_SECRET: SomeStaticAuthSecret
|
||||||
MAX_PER_GROUP: "MAX_PER_GROUP"
|
MAX_PER_GROUP: "MAX_PER_GROUP"
|
||||||
|
REDIS_HOST: redis
|
||||||
|
NODE_ENV: development
|
||||||
|
STORE_VARIABLES_FOR_LOCAL_MAPS: "true"
|
||||||
volumes:
|
volumes:
|
||||||
- ./back:/usr/src/app
|
- ./back:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
@ -155,6 +180,31 @@ services:
|
|||||||
- ./front:/usr/src/front
|
- ./front:/usr/src/front
|
||||||
- ./pusher:/usr/src/pusher
|
- ./pusher:/usr/src/pusher
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:6
|
||||||
|
|
||||||
|
redisinsight:
|
||||||
|
image: redislabs/redisinsight:latest
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.redisinsight.entryPoints=web"
|
||||||
|
- "traefik.http.services.redisinsight.loadbalancer.server.port=8001"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.rule=Host(`redis.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.redisinsight-ssl.service=redisinsight"
|
||||||
|
|
||||||
|
icon:
|
||||||
|
image: matthiasluedtke/iconserver:v3.13.0
|
||||||
|
labels:
|
||||||
|
- "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.icon.entryPoints=web"
|
||||||
|
- "traefik.http.services.icon.loadbalancer.server.port=8080"
|
||||||
|
- "traefik.http.routers.icon-ssl.rule=Host(`icon.workadventure.localhost`)"
|
||||||
|
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
|
||||||
|
- "traefik.http.routers.icon-ssl.tls=true"
|
||||||
|
- "traefik.http.routers.icon-ssl.service=icon"
|
||||||
|
|
||||||
# coturn:
|
# coturn:
|
||||||
# image: coturn/coturn:4.5.2
|
# image: coturn/coturn:4.5.2
|
||||||
# command:
|
# command:
|
||||||
|
33
docs/maps/animations.md
Normal file
33
docs/maps/animations.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# Animating WorkAdventure maps
|
||||||
|
|
||||||
|
A tile can run an animation in loops, for example to render water or blinking lights. Each animation frame is a single
|
||||||
|
32x32 tile. To create an animation, edit the tileset in Tiled and click on the tile to animate (or pick a free tile to
|
||||||
|
not overwrite existing ones) and click on the animation editor:
|
||||||
|
|
||||||
|
|
||||||
|
<div class="px-5 card rounded d-inline-block">
|
||||||
|
<img class="document-img" src="images/anims/camera.png" alt="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
You can now add all tiles that should be part of the animation via drag and drop to the "playlist" and adjust the frame duration:
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<figure class="figure">
|
||||||
|
<img class="figure-img img-fluid rounded" src="images/anims/animation_editor.png" alt="" />
|
||||||
|
<figcaption class="figure-caption">The tile animation editor</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
You can preview animations directly in Tiled, using the "Show tile animations" option:
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<figure class="figure">
|
||||||
|
<img class="figure-img img-fluid rounded" src="images/anims/settings_show_animations.png" alt="" />
|
||||||
|
<figcaption class="figure-caption">The Show Tile Animations option</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{.alert.alert-info}
|
||||||
|
**Tip:** The engine does tile-updates every 100ms, animations with a shorter frame duration will most likely not look that good or may even do not work.
|
37
docs/maps/api-chat.md
Normal file
37
docs/maps/api-chat.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API Chat functions reference
|
||||||
|
|
||||||
|
### Sending a message in the chat
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.chat.sendChatMessage(message: string, author: string): void
|
||||||
|
```
|
||||||
|
|
||||||
|
Sends a message in the chat. The message is only visible in the browser of the current user.
|
||||||
|
|
||||||
|
* **message**: the message to be displayed in the chat
|
||||||
|
* **author**: the name displayed for the author of the message. It does not have to be a real user.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.chat.sendChatMessage('Hello world', 'Mr Robot');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listening to messages from the chat
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.chat.onChatMessage(callback: (message: string) => void): void
|
||||||
|
```
|
||||||
|
|
||||||
|
Listens to messages typed by the current user and calls the callback. Messages from other users in the chat cannot be listened to.
|
||||||
|
|
||||||
|
* **callback**: the function that will be called when a message is received. It contains the message typed by the user.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.chat.onChatMessage((message => {
|
||||||
|
console.log('The user typed a message', message);
|
||||||
|
}));
|
||||||
|
```
|
29
docs/maps/api-controls.md
Normal file
29
docs/maps/api-controls.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API Controls functions Reference
|
||||||
|
|
||||||
|
### Disabling / restoring controls
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.controls.disablePlayerControls(): void
|
||||||
|
WA.controls.restorePlayerControls(): void
|
||||||
|
```
|
||||||
|
|
||||||
|
These 2 methods can be used to completely disable player controls and to enable them again.
|
||||||
|
|
||||||
|
When controls are disabled, the user cannot move anymore using keyboard input. This can be useful in a "First Time User Experience" part, to display an important message to a user before letting him/her move again.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.room.onEnterLayer('myZone').subscribe(() => {
|
||||||
|
WA.controls.disablePlayerControls();
|
||||||
|
WA.ui.openPopup("popupRectangle", 'This is an imporant message!', [{
|
||||||
|
label: "Got it!",
|
||||||
|
className: "primary",
|
||||||
|
callback: (popup) => {
|
||||||
|
WA.controls.restorePlayerControls();
|
||||||
|
popup.close();
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
})
|
||||||
|
```
|
23
docs/maps/api-deprecated.md
Normal file
23
docs/maps/api-deprecated.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API Reference - Deprecated functions
|
||||||
|
|
||||||
|
The list of functions below is **deprecated**. You should not use those but. use the replacement functions.
|
||||||
|
|
||||||
|
- Method `WA.sendChatMessage` is deprecated. It has been renamed to [`WA.chat.sendChatMessage`](api-chat.md#sending-a-message-in-the-chat).
|
||||||
|
- Method `WA.disablePlayerControls` is deprecated. It has been renamed to [`WA.controls.disablePlayerControls`](api-controls.md#disabling--restoring-controls).
|
||||||
|
- Method `WA.restorePlayerControls` is deprecated. It has been renamed to [`WA.controls.restorePlayerControls`](api-controls.md#disabling--restoring-controls).
|
||||||
|
- Method `WA.displayBubble` is deprecated. It has been renamed to `WA.ui.displayBubble`.
|
||||||
|
- Method `WA.removeBubble` is deprecated. It has been renamed to `WA.ui.removeBubble`.
|
||||||
|
- Method `WA.openTab` is deprecated. It has been renamed to [`WA.nav.openTab`](api-nav.md#opening-a-web-page-in-a-new-tab).
|
||||||
|
- Method `WA.loadSound` is deprecated. It has been renamed to [`WA.sound.loadSound`](api-sound.md#load-a-sound-from-an-url).
|
||||||
|
- Method `WA.goToPage` is deprecated. It has been renamed to [`WA.nav.goToPage`](api-nav.md#opening-a-web-page-in-the-current-tab).
|
||||||
|
- Method `WA.goToRoom` is deprecated. It has been renamed to [`WA.nav.goToRoom`](api-nav.md#going-to-a-different-map-from-the-script).
|
||||||
|
- Method `WA.openCoWebSite` is deprecated. It has been renamed to [`WA.nav.openCoWebSite`](api-nav.md#openingclosing-web-page-in-co-websites).
|
||||||
|
- Method `WA.closeCoWebSite` is deprecated. It has been remove and [replace by a function close](api-nav.md#openingclosing-web-page-in-co-websites).
|
||||||
|
- Method `WA.openPopup` is deprecated. It has been renamed to [`WA.ui.openPopup`](api-ui.md#opening-a-popup).
|
||||||
|
- Method `WA.onChatMessage` is deprecated. It has been renamed to [`WA.chat.onChatMessage`](api-chat.md#listening-to-messages-from-the-chat).
|
||||||
|
- Method `WA.onEnterZone` is deprecated. It has been renamed to [`WA.room.onEnterZone`](api-room.md#detecting-when-the-user-entersleaves-a-layer).
|
||||||
|
- Method `WA.onLeaveZone` is deprecated. It has been renamed to [`WA.room.onLeaveZone`](api-room.md#detecting-when-the-user-entersleaves-a-layer).
|
||||||
|
- Method `WA.ui.registerMenuCommand` parameter `callback` is deprecated. Use [`WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions)`](api-ui.md#add-custom-menu).
|
||||||
|
- Method `WA.room.onEnterZone` is deprecated. Use instead [`WA.room.onEnterLayer`](api-room.md#detecting-when-the-user-entersleaves-a-layer).
|
||||||
|
- Method `WA.room.onLeaveZone` is deprecated. Use instead [`WA.room.onLeaveLayer`](api-room.md#detecting-when-the-user-entersleaves-a-layer).
|
82
docs/maps/api-nav.md
Normal file
82
docs/maps/api-nav.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API Navigation functions reference
|
||||||
|
|
||||||
|
### Opening a web page in a new tab
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.nav.openTab(url: string): void
|
||||||
|
```
|
||||||
|
|
||||||
|
Opens the webpage at "url" in your browser, in a new tab.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.nav.openTab('https://www.wikipedia.org/');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opening a web page in the current tab
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.nav.goToPage(url: string): void
|
||||||
|
```
|
||||||
|
|
||||||
|
Opens the webpage at "url" in your browser in place of WorkAdventure. WorkAdventure will be completely unloaded.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.nav.goToPage('https://www.wikipedia.org/');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Going to a different map from the script
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
WA.nav.goToRoom(url: string): void
|
||||||
|
```
|
||||||
|
|
||||||
|
Load the map at url without unloading workadventure
|
||||||
|
|
||||||
|
relative urls: "../subFolder/map.json[#start-layer-name]"
|
||||||
|
global urls: "/_/global/domain/path/map.json[#start-layer-name]"
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.nav.goToRoom("/@/tcm/workadventure/floor0") // workadventure urls
|
||||||
|
WA.nav.goToRoom('../otherMap/map.json');
|
||||||
|
WA.nav.goToRoom("/_/global/<path to global map>.json#start-layer-2")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opening/closing web page in Co-Websites
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.nav.openCoWebSite(url: string, allowApi: boolean = false, allowPolicy: string = "", position: number = 0): Promise<CoWebsite>
|
||||||
|
```
|
||||||
|
|
||||||
|
Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame. `allowApi` allows the webpage to use the "IFrame API" and execute script (it is equivalent to putting the `openWebsiteAllowApi` property in the map). `allowPolicy` grants additional access rights to the iFrame. The `allowPolicy` parameter is turned into an [`allow` feature policy in the iFrame](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-allow), position in whitch slot the web page will be open.
|
||||||
|
You can have only 5 co-wbesites open simultaneously.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const coWebsite = await WA.nav.openCoWebSite('https://www.wikipedia.org/');
|
||||||
|
const coWebsiteWorkAdventure = await WA.nav.openCoWebSite('https://workadventu.re/', true, "", 1);
|
||||||
|
// ...
|
||||||
|
coWebsite.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get all Co-Websites
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.nav.getCoWebSites(): Promise<CoWebsite[]>
|
||||||
|
```
|
||||||
|
|
||||||
|
Get all opened co-websites with their ids and positions.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const coWebsites = await WA.nav.getCowebSites();
|
||||||
|
```
|
80
docs/maps/api-player.md
Normal file
80
docs/maps/api-player.md
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API Player functions Reference
|
||||||
|
|
||||||
|
### Get the player name
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.player.name: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
The player name is available from the `WA.player.name` property.
|
||||||
|
|
||||||
|
{.alert.alert-info}
|
||||||
|
You need to wait for the end of the initialization before accessing `WA.player.name`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('Player name: ', WA.player.name);
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get the player ID
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.player.id: string|undefined;
|
||||||
|
```
|
||||||
|
|
||||||
|
The player ID is available from the `WA.player.id` property.
|
||||||
|
This is a unique identifier for a given player. Anonymous player might not have an id.
|
||||||
|
|
||||||
|
{.alert.alert-info}
|
||||||
|
You need to wait for the end of the initialization before accessing `WA.player.id`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('Player ID: ', WA.player.id);
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get the tags of the player
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.player.tags: string[];
|
||||||
|
```
|
||||||
|
|
||||||
|
The player tags are available from the `WA.player.tags` property.
|
||||||
|
They represent a set of rights the player acquires after login in.
|
||||||
|
|
||||||
|
{.alert.alert-warn}
|
||||||
|
Tags attributed to a user depend on the authentication system you are using. For the hosted version
|
||||||
|
of WorkAdventure, you can define tags related to the user in the [administration panel](https://workadventu.re/admin-guide/manage-members).
|
||||||
|
|
||||||
|
{.alert.alert-info}
|
||||||
|
You need to wait for the end of the initialization before accessing `WA.player.tags`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('Tags: ', WA.player.tags);
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listen to player movement
|
||||||
|
```
|
||||||
|
WA.player.onPlayerMove(callback: HasPlayerMovedEventCallback): void;
|
||||||
|
```
|
||||||
|
Listens to the movement of the current user and calls the callback. Sends an event when the user stops moving, changes direction and every 200ms when moving in the same direction.
|
||||||
|
|
||||||
|
The event has the following attributes :
|
||||||
|
* **moving (boolean):** **true** when the current player is moving, **false** otherwise.
|
||||||
|
* **direction (string):** **"right"** | **"left"** | **"down"** | **"top"** the direction where the current player is moving.
|
||||||
|
* **x (number):** coordinate X of the current player.
|
||||||
|
* **y (number):** coordinate Y of the current player.
|
||||||
|
* **oldX (number):** old coordinate X of the current player.
|
||||||
|
* **oldY (number):** old coordinate Y of the current player.
|
||||||
|
|
||||||
|
**callback:** the function that will be called when the current player is moving. It contains the event.
|
||||||
|
|
||||||
|
Example :
|
||||||
|
```javascript
|
||||||
|
WA.player.onPlayerMove(console.log);
|
||||||
|
```
|
14
docs/maps/api-reference.md
Normal file
14
docs/maps/api-reference.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API Reference
|
||||||
|
|
||||||
|
- [Start / Init functions](api-start.md)
|
||||||
|
- [Navigation functions](api-nav.md)
|
||||||
|
- [Chat functions](api-chat.md)
|
||||||
|
- [Room functions](api-room.md)
|
||||||
|
- [State related functions](api-state.md)
|
||||||
|
- [Player functions](api-player.md)
|
||||||
|
- [UI functions](api-ui.md)
|
||||||
|
- [Sound functions](api-sound.md)
|
||||||
|
- [Controls functions](api-controls.md)
|
||||||
|
|
||||||
|
- [List of deprecated functions](api-deprecated.md)
|
269
docs/maps/api-room.md
Normal file
269
docs/maps/api-room.md
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API Room functions Reference
|
||||||
|
|
||||||
|
### Working with group layers
|
||||||
|
If you use group layers in your map, to reference a layer in a group you will need to use a `/` to join layer names together.
|
||||||
|
|
||||||
|
Example :
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<img src="images/groupLayer.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The name of the layers of this map are :
|
||||||
|
* `entries/start`
|
||||||
|
* `bottom/ground/under`
|
||||||
|
* `bottom/build/carpet`
|
||||||
|
* `wall`
|
||||||
|
|
||||||
|
### Detecting when the user enters/leaves a layer
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.onEnterLayer(name: string): Subscription
|
||||||
|
WA.room.onLeaveLayer(name: string): Subscription
|
||||||
|
```
|
||||||
|
|
||||||
|
Listens to the position of the current user. The event is triggered when the user enters or leaves a given layer.
|
||||||
|
|
||||||
|
* **name**: the name of the layer who as defined in Tiled.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.room.onEnterLayer('myLayer').subscribe(() => {
|
||||||
|
WA.chat.sendChatMessage("Hello!", 'Mr Robot');
|
||||||
|
});
|
||||||
|
|
||||||
|
WA.room.onLeaveLayer('myLayer').subscribe(() => {
|
||||||
|
WA.chat.sendChatMessage("Goodbye!", 'Mr Robot');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Show / Hide a layer
|
||||||
|
```
|
||||||
|
WA.room.showLayer(layerName : string): void
|
||||||
|
WA.room.hideLayer(layerName : string) : void
|
||||||
|
```
|
||||||
|
These 2 methods can be used to show and hide a layer.
|
||||||
|
if `layerName` is the name of a group layer, show/hide all the layer in that group layer.
|
||||||
|
|
||||||
|
Example :
|
||||||
|
```javascript
|
||||||
|
WA.room.showLayer('bottom');
|
||||||
|
//...
|
||||||
|
WA.room.hideLayer('bottom');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set/Create properties in a layer
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.setProperty(layerName : string, propertyName : string, propertyValue : string | number | boolean | undefined) : void;
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the value of the `propertyName` property of the layer `layerName` at `propertyValue`. If the property doesn't exist, create the property `propertyName` and set the value of the property at `propertyValue`.
|
||||||
|
|
||||||
|
Note :
|
||||||
|
To unset a property from a layer, use `setProperty` with `propertyValue` set to `undefined`.
|
||||||
|
|
||||||
|
Example :
|
||||||
|
```javascript
|
||||||
|
WA.room.setProperty('wikiLayer', 'openWebsite', 'https://www.wikipedia.org/');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get the room id
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.id: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
The ID of the current room is available from the `WA.room.id` property.
|
||||||
|
|
||||||
|
{.alert.alert-info}
|
||||||
|
You need to wait for the end of the initialization before accessing `WA.room.id`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('Room id: ', WA.room.id);
|
||||||
|
// Will output something like: 'https://play.workadventu.re/@/myorg/myworld/myroom', or 'https://play.workadventu.re/_/global/mymap.org/map.json"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get the map URL
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.mapURL: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
The URL of the map is available from the `WA.room.mapURL` property.
|
||||||
|
|
||||||
|
{.alert.alert-info}
|
||||||
|
You need to wait for the end of the initialization before accessing `WA.room.mapURL`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('Map URL: ', WA.room.mapURL);
|
||||||
|
// Will output something like: 'https://mymap.org/map.json"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Getting map data
|
||||||
|
```
|
||||||
|
WA.room.getTiledMap(): Promise<ITiledMap>
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns a promise that resolves to the JSON map file.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const map = await WA.room.getTiledMap();
|
||||||
|
console.log("Map generated with Tiled version ", map.tiledversion);
|
||||||
|
```
|
||||||
|
|
||||||
|
Check the [Tiled documentation to learn more about the format of the JSON map](https://doc.mapeditor.org/en/stable/reference/json-map-format/).
|
||||||
|
|
||||||
|
### Changing tiles
|
||||||
|
```
|
||||||
|
WA.room.setTiles(tiles: TileDescriptor[]): void
|
||||||
|
```
|
||||||
|
Replace the tile at the `x` and `y` coordinates in the layer named `layer` by the tile with the id `tile`.
|
||||||
|
|
||||||
|
If `tile` is a string, it's not the id of the tile but the value of the property `name`.
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<img src="images/nameIndexProperty.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
`TileDescriptor` has the following attributes :
|
||||||
|
* **x (number) :** The coordinate x of the tile that you want to replace.
|
||||||
|
* **y (number) :** The coordinate y of the tile that you want to replace.
|
||||||
|
* **tile (number | string) :** The id of the tile that will be placed in the map.
|
||||||
|
* **layer (string) :** The name of the layer where the tile will be placed.
|
||||||
|
|
||||||
|
**Important !** : If you use `tile` as a number, be sure to add the `firstgid` of the tileset of the tile that you want to the id of the tile in Tiled Editor.
|
||||||
|
|
||||||
|
Note: If you want to unset a tile, use `setTiles` with `tile` set to `null`.
|
||||||
|
|
||||||
|
Example :
|
||||||
|
```javascript
|
||||||
|
WA.room.setTiles([
|
||||||
|
{x: 6, y: 4, tile: 'blue', layer: 'setTiles'},
|
||||||
|
{x: 7, y: 4, tile: 109, layer: 'setTiles'},
|
||||||
|
{x: 8, y: 4, tile: 109, layer: 'setTiles'},
|
||||||
|
{x: 9, y: 4, tile: 'blue', layer: 'setTiles'}
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Loading a tileset
|
||||||
|
```
|
||||||
|
WA.room.loadTileset(url: string): Promise<number>
|
||||||
|
```
|
||||||
|
Load a tileset in JSON format from an url and return the id of the first tile of the loaded tileset.
|
||||||
|
|
||||||
|
You can create a tileset file in Tile Editor.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.room.loadTileset("Assets/Tileset.json").then((firstId) => {
|
||||||
|
WA.room.setTiles([{x: 4, y: 4, tile: firstId, layer: 'bottom'}]);
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Embedding websites in a map
|
||||||
|
|
||||||
|
You can use the scripting API to embed websites in a map, or to edit websites that are already embedded (using the ["website" objects](website-in-map.md)).
|
||||||
|
|
||||||
|
### Getting an instance of a website already embedded in the map
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.website.get(objectName: string): Promise<EmbeddedWebsite>
|
||||||
|
```
|
||||||
|
|
||||||
|
You can get an instance of an embedded website by using the `WA.room.website.get()` method.
|
||||||
|
It returns a promise of an `EmbeddedWebsite` instance.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Get an existing website object where 'my_website' is the name of the object (on any layer object of the map)
|
||||||
|
const website = await WA.room.website.get('my_website');
|
||||||
|
website.url = 'https://example.com';
|
||||||
|
website.visible = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Adding a new website in a map
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.website.create(website: CreateEmbeddedWebsiteEvent): EmbeddedWebsite
|
||||||
|
|
||||||
|
interface CreateEmbeddedWebsiteEvent {
|
||||||
|
name: string; // A unique name for this iframe
|
||||||
|
url: string; // The URL the iframe points to.
|
||||||
|
position: {
|
||||||
|
x: number, // In pixels, relative to the map coordinates
|
||||||
|
y: number, // In pixels, relative to the map coordinates
|
||||||
|
width: number, // In pixels, sensitive to zoom level
|
||||||
|
height: number, // In pixels, sensitive to zoom level
|
||||||
|
},
|
||||||
|
visible?: boolean, // Whether to display the iframe or not
|
||||||
|
allowApi?: boolean, // Whether the scripting API should be available to the iframe
|
||||||
|
allow?: string, // The list of feature policies allowed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can create an instance of an embedded website by using the `WA.room.website.create()` method.
|
||||||
|
It returns an `EmbeddedWebsite` instance.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create a new website object
|
||||||
|
const website = WA.room.website.create({
|
||||||
|
name: "my_website",
|
||||||
|
url: "https://example.com",
|
||||||
|
position: {
|
||||||
|
x: 64,
|
||||||
|
y: 128,
|
||||||
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
},
|
||||||
|
visible: true,
|
||||||
|
allowApi: true,
|
||||||
|
allow: "fullscreen",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deleting a website from a map
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.room.website.delete(name: string): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `WA.room.website.delete` to completely remove an embedded website from your map.
|
||||||
|
|
||||||
|
|
||||||
|
### The EmbeddedWebsite class
|
||||||
|
|
||||||
|
Instances of the `EmbeddedWebsite` class represent the website displayed on the map.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class EmbeddedWebsite {
|
||||||
|
readonly name: string;
|
||||||
|
url: string;
|
||||||
|
visible: boolean;
|
||||||
|
allow: string;
|
||||||
|
allowApi: boolean;
|
||||||
|
x: number; // In pixels, relative to the map coordinates
|
||||||
|
y: number; // In pixels, relative to the map coordinates
|
||||||
|
width: number; // In pixels, sensitive to zoom level
|
||||||
|
height: number; // In pixels, sensitive to zoom level
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When you modify a property of an `EmbeddedWebsite` instance, the iframe is automatically modified in the map.
|
||||||
|
|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
The websites you add/edit/delete via the scripting API are only shown locally. If you want them
|
||||||
|
to be displayed for every player, you can use [variables](api-start.md) to share a common state
|
||||||
|
between all users.
|
||||||
|
|
34
docs/maps/api-sound.md
Normal file
34
docs/maps/api-sound.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API Sound functions Reference
|
||||||
|
|
||||||
|
### Load a sound from an url
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.sound.loadSound(url: string): Sound
|
||||||
|
```
|
||||||
|
|
||||||
|
Load a sound from an url
|
||||||
|
|
||||||
|
Please note that `loadSound` returns an object of the `Sound` class
|
||||||
|
|
||||||
|
The `Sound` class that represents a loaded sound contains two methods: `play(soundConfig : SoundConfig|undefined)` and `stop()`
|
||||||
|
|
||||||
|
The parameter soundConfig is optional, if you call play without a Sound config the sound will be played with the basic configuration.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
var mySound = WA.sound.loadSound("Sound.ogg");
|
||||||
|
var config = {
|
||||||
|
volume : 0.5,
|
||||||
|
loop : false,
|
||||||
|
rate : 1,
|
||||||
|
detune : 1,
|
||||||
|
delay : 0,
|
||||||
|
seek : 0,
|
||||||
|
mute : false
|
||||||
|
}
|
||||||
|
mySound.play(config);
|
||||||
|
// ...
|
||||||
|
mySound.stop();
|
||||||
|
```
|
30
docs/maps/api-start.md
Normal file
30
docs/maps/api-start.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API start functions Reference
|
||||||
|
|
||||||
|
### Waiting for WorkAdventure API to be available
|
||||||
|
|
||||||
|
When your script / iFrame loads WorkAdventure, it takes a few milliseconds for your script / iFrame to exchange
|
||||||
|
data with WorkAdventure. You should wait for the WorkAdventure API to be fully ready using the `WA.onInit()` method.
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.onInit(): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
Some properties (like the current user name, or the room ID) are not available until `WA.onInit` has completed.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
WA.onInit().then(() => {
|
||||||
|
console.log('Current player name: ', WA.player.name);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Or the same code, using await/async:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
(async () => {
|
||||||
|
await WA.onInit();
|
||||||
|
console.log('Current player name: ', WA.player.name);
|
||||||
|
})();
|
||||||
|
```
|
104
docs/maps/api-state.md
Normal file
104
docs/maps/api-state.md
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API state related functions Reference
|
||||||
|
|
||||||
|
## Saving / loading state
|
||||||
|
|
||||||
|
The `WA.state` functions allow you to easily share a common state between all the players in a given room.
|
||||||
|
Moreover, `WA.state` functions can be used to persist this state across reloads.
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.state.saveVariable(key : string, data : unknown): void
|
||||||
|
WA.state.loadVariable(key : string) : unknown
|
||||||
|
WA.state.hasVariable(key : string) : boolean
|
||||||
|
WA.state.onVariableChange(key : string).subscribe((data: unknown) => {}) : Subscription
|
||||||
|
WA.state.[any property]: unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
These methods and properties can be used to save, load and track changes in [variables related to the current room](variables.md).
|
||||||
|
|
||||||
|
Variables stored in `WA.state` can be any value that is serializable in JSON.
|
||||||
|
|
||||||
|
Please refrain from storing large amounts of data in a room. Those functions are typically useful for saving or restoring
|
||||||
|
configuration / metadata.
|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
We are in the process of fine-tuning variables, and we will eventually put limits on the maximum size a variable can hold. We will also put limits on the number of calls you can make to saving variables, so don't change the value of a variable every 10ms, this will fail in the future.
|
||||||
|
|
||||||
|
|
||||||
|
Example :
|
||||||
|
```javascript
|
||||||
|
WA.state.saveVariable('config', {
|
||||||
|
'bottomExitUrl': '/@/org/world/castle',
|
||||||
|
'topExitUrl': '/@/org/world/tower',
|
||||||
|
'enableBirdSound': true
|
||||||
|
}).catch(e => console.error('Something went wrong while saving variable', e));
|
||||||
|
//...
|
||||||
|
let config = WA.state.loadVariable('config');
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use the shortcut properties to load and save variables. The code above is similar to:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.state.config = {
|
||||||
|
'bottomExitUrl': '/@/org/world/castle',
|
||||||
|
'topExitUrl': '/@/org/world/tower',
|
||||||
|
'enableBirdSound': true
|
||||||
|
};
|
||||||
|
|
||||||
|
//...
|
||||||
|
let config = WA.state.config;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `saveVariable` returns a promise that will fail in case the variable cannot be saved. This
|
||||||
|
can happen if your user does not have the required rights (more on that in the next chapter).
|
||||||
|
In contrast, if you use the WA.state properties, you cannot access the promise and therefore cannot
|
||||||
|
know for sure if your variable was properly saved.
|
||||||
|
|
||||||
|
If you are using Typescript, please note that the type of variables is `unknown`. This is
|
||||||
|
for security purpose, as we don't know the type of the variable. In order to use the returned value,
|
||||||
|
you will need to cast it to the correct type (or better, use a [Type guard](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to actually check at runtime
|
||||||
|
that you get the expected type).
|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
For security reasons, the list of variables you are allowed to access and modify is **restricted** (otherwise, anyone on your map could set any data).
|
||||||
|
Variables storage is subject to an authorization process. Read below to learn more.
|
||||||
|
|
||||||
|
## Defining a variable
|
||||||
|
|
||||||
|
Out of the box, you cannot edit *any* variable. Variables MUST be declared in the map.
|
||||||
|
|
||||||
|
Check the [dedicated variables page](variables.md) to learn how to declare a variable in a map.
|
||||||
|
|
||||||
|
## Tracking variables changes
|
||||||
|
|
||||||
|
The properties of the `WA.state` object are shared in real-time between users of a same room. You can listen to modifications
|
||||||
|
of any property of `WA.state` by using the `WA.state.onVariableChange()` method.
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.state.onVariableChange(name: string): Observable<unknown>
|
||||||
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
WA.state.onVariableChange('config').subscribe((value) => {
|
||||||
|
console.log('Variable "config" changed. New value: ', value);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `WA.state.onVariableChange` method returns an [RxJS `Observable` object](https://rxjs.dev/guide/observable). This is
|
||||||
|
an object on which you can add subscriptions using the `subscribe` method.
|
||||||
|
|
||||||
|
### Stopping tracking variables
|
||||||
|
|
||||||
|
If you want to stop tracking a variable change, the `subscribe` method returns a subscription object with an `unsubscribe` method.
|
||||||
|
|
||||||
|
**Example with unsubscription:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const subscription = WA.state.onVariableChange('config').subscribe((value) => {
|
||||||
|
console.log('Variable "config" changed. New value: ', value);
|
||||||
|
});
|
||||||
|
// Later:
|
||||||
|
subscription.unsubscribe();
|
||||||
|
```
|
164
docs/maps/api-ui.md
Normal file
164
docs/maps/api-ui.md
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# API UI functions Reference
|
||||||
|
|
||||||
|
### Opening a popup
|
||||||
|
|
||||||
|
In order to open a popup window, you must first define the position of the popup on your map.
|
||||||
|
|
||||||
|
You can position this popup by using a "rectangle" object in Tiled that you will place on an "object" layer.
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<img src="images/screen_popup_tiled.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<img src="images/screen_popup_in_game.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.ui.openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup
|
||||||
|
```
|
||||||
|
|
||||||
|
* **targetObject**: the name of the rectangle object defined in Tiled.
|
||||||
|
* **message**: the message to display in the popup.
|
||||||
|
* **buttons**: an array of action buttons defined underneath the popup.
|
||||||
|
|
||||||
|
Action buttons are `ButtonDescriptor` objects containing these properties.
|
||||||
|
|
||||||
|
* **label (_string_)**: The label of the button.
|
||||||
|
* **className (_string_)**: The visual type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled".
|
||||||
|
* **callback (_(popup: Popup)=>void_)**: Callback called when the button is pressed.
|
||||||
|
|
||||||
|
Please note that `openPopup` returns an object of the `Popup` class. Also, the callback called when a button is clicked is passed a `Popup` object.
|
||||||
|
|
||||||
|
The `Popup` class that represents an open popup contains a single method: `close()`. This will obviously close the popup when called.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class Popup {
|
||||||
|
/**
|
||||||
|
* Closes the popup
|
||||||
|
*/
|
||||||
|
close() {};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let helloWorldPopup;
|
||||||
|
|
||||||
|
// Open the popup when we enter a given zone
|
||||||
|
helloWorldPopup = WA.room.onEnterLayer("myZone").subscribe(() => {
|
||||||
|
WA.ui.openPopup("popupRectangle", 'Hello world!', [{
|
||||||
|
label: "Close",
|
||||||
|
className: "primary",
|
||||||
|
callback: (popup) => {
|
||||||
|
// Close the popup when the "Close" button is pressed.
|
||||||
|
popup.close();
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close the popup when we leave the zone.
|
||||||
|
WA.room.onLeaveLayer("myZone").subscribe(() => {
|
||||||
|
helloWorldPopup.close();
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Add custom menu
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.ui.registerMenuCommand(commandDescriptor: string, options: MenuOptions): Menu
|
||||||
|
```
|
||||||
|
Add a custom menu item containing the text `commandDescriptor` in the navbar of the menu.
|
||||||
|
`options` attribute accepts an object with three properties :
|
||||||
|
- `callback : (commandDescriptor: string) => void` : A click on the custom menu will trigger the `callback`.
|
||||||
|
- `iframe: string` : A click on the custom menu will open the `iframe` inside the menu.
|
||||||
|
- `allowApi?: boolean` : Allow the iframe of the custom menu to use the Scripting API.
|
||||||
|
|
||||||
|
Important : `options` accepts only `callback` or `iframe` not both.
|
||||||
|
|
||||||
|
Custom menu exist only until the map is unloaded, or you leave the iframe zone of the script.
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<img src="images/custom-menu-navbar.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<img src="images/custom-menu-iframe.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```javascript
|
||||||
|
const menu = WA.ui.registerMenuCommand('menu test',
|
||||||
|
{
|
||||||
|
callback: () => {
|
||||||
|
WA.chat.sendChatMessage('test');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Some time later, if you want to remove the menu:
|
||||||
|
menu.remove();
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that `registerMenuCommand` returns an object of the `Menu` class.
|
||||||
|
|
||||||
|
The `Menu` class contains a single method: `remove(): void`. This will obviously remove the menu when called.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class Menu {
|
||||||
|
/**
|
||||||
|
* Remove the menu
|
||||||
|
*/
|
||||||
|
remove() {};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Awaiting User Confirmation (with space bar)
|
||||||
|
|
||||||
|
```
|
||||||
|
WA.ui.displayActionMessage({
|
||||||
|
message: string,
|
||||||
|
callback: () => void,
|
||||||
|
type?: "message"|"warning",
|
||||||
|
}): ActionMessage
|
||||||
|
```
|
||||||
|
|
||||||
|
Displays a message at the bottom of the screen (that will disappear when space bar is pressed).
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<img src="images/trigger_message.png" class="figure-img img-fluid rounded" alt="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const triggerMessage = WA.ui.displayActionMessage({
|
||||||
|
message: "press 'space' to confirm",
|
||||||
|
callback: () => {
|
||||||
|
WA.chat.sendChatMessage("confirmed", "trigger message logic")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
// later
|
||||||
|
triggerMessage.remove();
|
||||||
|
}, 1000)
|
||||||
|
```
|
||||||
|
|
||||||
|
Please note that `displayActionMessage` returns an object of the `ActionMessage` class.
|
||||||
|
|
||||||
|
The `ActionMessage` class contains a single method: `remove(): Promise<void>`. This will obviously remove the message when called.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ActionMessage {
|
||||||
|
/**
|
||||||
|
* Hides the message
|
||||||
|
*/
|
||||||
|
remove() {};
|
||||||
|
}
|
||||||
|
```
|
67
docs/maps/entry-exit.md
Normal file
67
docs/maps/entry-exit.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# Entries and exits
|
||||||
|
|
||||||
|
[Building your map - Defined entries and exits](https://www.youtube.com/watch?v=MuhVgu8H7U0)
|
||||||
|
|
||||||
|
## Defining a default entry point
|
||||||
|
|
||||||
|
In order to define a default start position, you MUST create a layer named "`start`" on your map. This layer MUST contain at least one tile. The players will start on the tile of this layer. If the layer contains many tiles, the players will start randomly on one of those tiles.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the screenshot above, the start layer is made of the 2 white tiles. These tiles are not visible to the end user because they are hidden below the "bottom" layer that displays the floor of the map.
|
||||||
|
|
||||||
|
{.alert.alert-info}
|
||||||
|
**Pro tip**: if you expect many people to connect to your map at the same time (for instance, if you are organizing a big event), consider making a large start zone. This way, users will not all appear at the same position and will not pop randomly in a chat with someone connecting at the same moment.
|
||||||
|
|
||||||
|
## Defining exits
|
||||||
|
|
||||||
|
In order to place an exit on your scene that leads to another scene:
|
||||||
|
|
||||||
|
* You must create a specific layer. When a character reaches ANY tile of that layer, it will exit the scene.
|
||||||
|
* In layer properties, you MUST add "`exitUrl`" property. It represents the URL of the next scene. You can put relative or absolute URLs.
|
||||||
|
* If you want to have multiple exits, you can create many layers. Each layer has a different key `exitUrl` and has tiles that represent exits to another scene.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
**Note:** in older releases of WorkAdventure, you could link to a map file directly using properties `exitSceneUrl` and `exitInstance`. Those properties are now **deprecated**. Use "`exitUrl`" instead.
|
||||||
|
|
||||||
|
## Understanding map URLs in WorkAdventure
|
||||||
|
|
||||||
|
There are 2 kinds of URLs in WorkAdventure:
|
||||||
|
|
||||||
|
* Public URLs are in the form `https://play.workadventu.re/_/[instance]/[server]/[path to map]`
|
||||||
|
* Private URLs (used in paid accounts) are in the form `https://play.workadventu.re/@/[organization]/[world]/[map]`
|
||||||
|
|
||||||
|
Assuming your JSON map is hosted at "`https://example.com/my/map.json`", then you can browse your map at "`https://play.workadventu.re/_/global/example.com/my/map.json`". Here, "global" is a name of an "instance" of your map. You can put anything instead of "global" here. People on the same instance of the map can see each others. If 2 users use 2 different instances, they are on the same map, but in 2 parallel universes. They cannot see each other.
|
||||||
|
|
||||||
|
## Defining several entry points
|
||||||
|
|
||||||
|
Often your map will have several exits, and therefore, several entry points. For instance, if there is an exit by a door that leads to the garden map, when you come back from the garden you expect to come back by the same door. Therefore, a map can have several entry points. Those entry points are "named" (they have a name).
|
||||||
|
|
||||||
|
In order to create a named entry point:
|
||||||
|
|
||||||
|
You can create a new layer for your entry point or use an existing layer with named tiles.
|
||||||
|
|
||||||
|
* If you don't use the layer named "`start`", you MUST add a boolean "`startLayer`" property to the layer properties. It MUST be set to true.
|
||||||
|
* If you use this method, when a character enters the map by this entry point, it will enter randomly on ANY tile of that layer. The name of the entry point is the name of that layer.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can also use the tiles properties to create entry point.
|
||||||
|
|
||||||
|
* To do that, you will need to have a layer named "`start`" or with the "`startLayer`" property. Then you MUST add a string "`start`" property to a tile than you use in that layer. The name of the entry point is the value that property.
|
||||||
|
* If you use this method, when a character enters the map by this entry point, it will enter on ANY tile of the same kind in that layer.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Notes :
|
||||||
|
|
||||||
|
* Two tiles with a string "start" property with different value can be in the same layer of entries.
|
||||||
|
* A tile with a string "start" property that is not in a layer of entries won't usable as an entry point.
|
||||||
|
|
||||||
|
How to use entry point :
|
||||||
|
|
||||||
|
* To enter via this entry point, simply add a hash with the entry point name to the URL ("#[_entryPointName_]"). For instance: "`https://workadventu.re/_/global/mymap.com/path/map.json#my-entry-point`".
|
||||||
|
* You can of course use the "#" notation in an exit scene URL (so an exit scene URL will point to a given entry scene URL)
|
25
docs/maps/hosting.md
Normal file
25
docs/maps/hosting.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{.section-title.accent.text-primary}
|
||||||
|
# Hosting your map
|
||||||
|
|
||||||
|
The [Getting Started](.) page proposes to use a "starter kit" that is relying on GitHub pages for hosting the map. This is a fairly good solution as GitHub pages offers a free and performant hosting.
|
||||||
|
|
||||||
|
But using GitHub pages is not necessary. You can host your maps on any webserver.
|
||||||
|
|
||||||
|
{.alert.alert-warning}
|
||||||
|
If you decide to host your maps on your own webserver, you must **configure CORS headers** in your browser to allow access from WorkAdventure.
|
||||||
|
|
||||||
|
## Configuring CORS headers
|
||||||
|
|
||||||
|
CORS headers ([Cross Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)) are useful when a website want to make some resources accessible to another website. This is exactly what we want to do. We want the map you are designing to be accessible from the WorkAdventure domain (`play.workadventu.re`).
|
||||||
|
|
||||||
|
### Enabling CORS for Apache
|
||||||
|
|
||||||
|
In order to enable CORS in your Apache configuration, you will need to ensure the `headers` module is enabled.
|
||||||
|
|
||||||
|
In your Apache configuration file, simply add the following line inside either the `<Directory>`, `<Location>`, `<Files>` or `<VirtualHost>` sections, or within a `.htaccess` file.
|
||||||
|
|
||||||
|
Header set Access-Control-Allow-Origin "*"
|
||||||
|
|
||||||
|
### Enabling CORS on another webserver
|
||||||
|
|
||||||
|
Check out [enable-cors.org](https://enable-cors.org/server.html) which has detailed instructions on how to enable CORS on many different webservers.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user