Compare commits
532 Commits
v0.5.0-alp
...
v0.6.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0187da5f24 | ||
|
|
2e1c37ffff | ||
|
|
4b0189dd55 | ||
|
|
d4a39674de | ||
|
|
07c6fd3711 | ||
|
|
cf5b72aa9a | ||
|
|
cdfbebd637 | ||
|
|
e86e3316a9 | ||
|
|
bb2dcbaea0 | ||
|
|
fefc7a69cd | ||
|
|
1b24cd36e0 | ||
|
|
6322c1f46d | ||
|
|
00a29bb6be | ||
|
|
3a029caf2b | ||
|
|
57263905e7 | ||
|
|
4f46012c11 | ||
|
|
7ea9a1e3e8 | ||
|
|
c7510e12c4 | ||
|
|
89b5d9294c | ||
|
|
339e1060b7 | ||
|
|
52e1dbd846 | ||
|
|
b4963f4ff7 | ||
|
|
838e8f6bde | ||
|
|
8f21514855 | ||
|
|
44041f33e3 | ||
|
|
5d9bc4473c | ||
|
|
a3e0d42e86 | ||
|
|
4a9c1d0d1b | ||
|
|
ff47f79c62 | ||
|
|
f46a391873 | ||
|
|
99b8ada42b | ||
|
|
f2227aa7cb | ||
|
|
a9f6c20113 | ||
|
|
e261e7a2c7 | ||
|
|
ae811aafd0 | ||
|
|
cc593b465d | ||
|
|
f81e8339e3 | ||
|
|
cdc5513726 | ||
|
|
a398338c02 | ||
|
|
26e2e81188 | ||
|
|
46d7bba87d | ||
|
|
57d5610a58 | ||
|
|
1968e1fdb7 | ||
|
|
f2584cf807 | ||
|
|
dd592d1d6d | ||
|
|
cfff08cdfd | ||
|
|
e83437c193 | ||
|
|
1d900a6a3c | ||
|
|
859c7bea8a | ||
|
|
1b9e90e786 | ||
|
|
41396367ac | ||
|
|
16c9a23d55 | ||
|
|
ce338a2a72 | ||
|
|
1f7f2b22e7 | ||
|
|
47c7f63d75 | ||
|
|
8b1e85fbb4 | ||
|
|
ff4e9fb027 | ||
|
|
d15ef40988 | ||
|
|
eadfb18f74 | ||
|
|
4b89bb087a | ||
|
|
f0a602b48b | ||
|
|
fe10da9a6c | ||
|
|
948303aac8 | ||
|
|
12df80da56 | ||
|
|
a48c238ed7 | ||
|
|
da6c7b4d7a | ||
|
|
15cbbacc2f | ||
|
|
c3f6a17bae | ||
|
|
0fbeb70865 | ||
|
|
02f98a4053 | ||
|
|
d2e2a13479 | ||
|
|
f32cb592e2 | ||
|
|
72342f3118 | ||
|
|
246fd5f6c4 | ||
|
|
715022def5 | ||
|
|
78b2bafde1 | ||
|
|
2d7ebac8e6 | ||
|
|
bcffd2eb99 | ||
|
|
459bf62fab | ||
|
|
bdaa671823 | ||
|
|
463bf9b116 | ||
|
|
0c7a94c062 | ||
|
|
af866f0665 | ||
|
|
2493c70241 | ||
|
|
3c3359999c | ||
|
|
2467004dc2 | ||
|
|
3a9f438390 | ||
|
|
3126de2c37 | ||
|
|
b7d026b98e | ||
|
|
3f8b178f88 | ||
|
|
6184f4ce4f | ||
|
|
fa736697a9 | ||
|
|
2c88c700ca | ||
|
|
0b2e9ce1ad | ||
|
|
ccafdf250e | ||
|
|
57b28faa74 | ||
|
|
d0d7c58af1 | ||
|
|
339fc979f5 | ||
|
|
47614c3102 | ||
|
|
94e1501329 | ||
|
|
dfb015acc2 | ||
|
|
f1daafbf9e | ||
|
|
432e10c205 | ||
|
|
62b626a931 | ||
|
|
9a2893c6bc | ||
|
|
3a538f98ed | ||
|
|
7c71708de7 | ||
|
|
b64d97e808 | ||
|
|
428a19789d | ||
|
|
7cc4055d94 | ||
|
|
114c93fa46 | ||
|
|
07801a0283 | ||
|
|
30c8ea583b | ||
|
|
d54a5f647e | ||
|
|
457ed05174 | ||
|
|
44c94da2d8 | ||
|
|
ec614e1912 | ||
|
|
559b509a03 | ||
|
|
e2e9209655 | ||
|
|
e55b72f73f | ||
|
|
85e30cc968 | ||
|
|
51dcfbab75 | ||
|
|
7c0aa45057 | ||
|
|
a5fdc2a9cc | ||
|
|
64fd2f9cf8 | ||
|
|
a77495c562 | ||
|
|
3880708e1d | ||
|
|
cc68d57f11 | ||
|
|
56f5a61362 | ||
|
|
40540c3637 | ||
|
|
82678235ab | ||
|
|
5123d21290 | ||
|
|
b0dcf515f0 | ||
|
|
9aafccc946 | ||
|
|
ccf98d7283 | ||
|
|
1b35c98be5 | ||
|
|
db23c3d35a | ||
|
|
1c6c1298d5 | ||
|
|
b9ebed14a5 | ||
|
|
7077f0f1f3 | ||
|
|
d1887c6cd3 | ||
|
|
9278c81611 | ||
|
|
dec9ec32df | ||
|
|
1967f738a8 | ||
|
|
1031600c63 | ||
|
|
24c697bea7 | ||
|
|
83665018d4 | ||
|
|
b093626a21 | ||
|
|
f3dc78b7cd | ||
|
|
f0cd4333c3 | ||
|
|
6d38a785ed | ||
|
|
e46f9fd94f | ||
|
|
6b620ddcef | ||
|
|
e5684ad947 | ||
|
|
dfabc07044 | ||
|
|
dca88e8272 | ||
|
|
9f947ae2c5 | ||
|
|
28156bcaf2 | ||
|
|
a14aacbcf9 | ||
|
|
321e026e43 | ||
|
|
30351f7b9d | ||
|
|
2393a1a114 | ||
|
|
479476561e | ||
|
|
a02f485040 | ||
|
|
2957e8ad73 | ||
|
|
f83abe15cb | ||
|
|
709cba45ce | ||
|
|
8a9a72fd50 | ||
|
|
13cb0a27a0 | ||
|
|
fa817e3cdd | ||
|
|
3bd8549f41 | ||
|
|
4e47655b17 | ||
|
|
18533d5944 | ||
|
|
c5839c94ca | ||
|
|
ffc146ff9b | ||
|
|
7b82e3d641 | ||
|
|
ab6cedb659 | ||
|
|
e60be94be0 | ||
|
|
f830b024d5 | ||
|
|
98519da7d9 | ||
|
|
cb3211cf46 | ||
|
|
c051153a1f | ||
|
|
956d7e0918 | ||
|
|
530a7aa834 | ||
|
|
052ca52cc3 | ||
|
|
2025b8a494 | ||
|
|
238233ca81 | ||
|
|
938c8259c3 | ||
|
|
35df5f25c7 | ||
|
|
cd84fb9fc1 | ||
|
|
8bcf12e317 | ||
|
|
10ed353e0a | ||
|
|
72d88200e4 | ||
|
|
6ef832b3a9 | ||
|
|
17d91563d5 | ||
|
|
fb65693627 | ||
|
|
079fc738ad | ||
|
|
ae0f3b0b02 | ||
|
|
b2c515f2e6 | ||
|
|
6f1469a551 | ||
|
|
1d441b86d2 | ||
|
|
b72e6e54e6 | ||
|
|
1ee679395d | ||
|
|
cf62ee5ccb | ||
|
|
995685adfe | ||
|
|
f0eb955628 | ||
|
|
923f661428 | ||
|
|
b562d11994 | ||
|
|
3554112f31 | ||
|
|
7cada2608f | ||
|
|
21d4acebc3 | ||
|
|
73bbaaf0af | ||
|
|
40105515d2 | ||
|
|
79f09deecb | ||
|
|
c4a7f8675f | ||
|
|
f9a5fee364 | ||
|
|
bdd9c58cae | ||
|
|
09368e87cf | ||
|
|
46858c45b8 | ||
|
|
fe1a96f850 | ||
|
|
581449d992 | ||
|
|
15e848d4bf | ||
|
|
0dbbf44ab2 | ||
|
|
c62a5d15ad | ||
|
|
53bd02a890 | ||
|
|
099aa26ef8 | ||
|
|
2598ea3577 | ||
|
|
7be5b36494 | ||
|
|
3dff4d1061 | ||
|
|
5b7037550d | ||
|
|
b94f21402c | ||
|
|
9f5875d4bb | ||
|
|
694b6101ca | ||
|
|
1737da9c1f | ||
|
|
bcf4fea598 | ||
|
|
90e41f3ce9 | ||
|
|
5f5fb51eae | ||
|
|
95f06d56d8 | ||
|
|
1c6788ea45 | ||
|
|
457a8a5cf8 | ||
|
|
282caf10a5 | ||
|
|
b2fc6c165b | ||
|
|
0a1fa9588f | ||
|
|
5f5bfadf2b | ||
|
|
b512e707a5 | ||
|
|
3678ddfa36 | ||
|
|
ab8aa89f2a | ||
|
|
fd22482f06 | ||
|
|
e741f7b557 | ||
|
|
a70b7ee150 | ||
|
|
87227c96ed | ||
|
|
91d12aeb4f | ||
|
|
b3f132201b | ||
|
|
24b9f552eb | ||
|
|
b4a8dec68c | ||
|
|
c140823bea | ||
|
|
f166cc93cd | ||
|
|
125c9767f1 | ||
|
|
7b7c978db6 | ||
|
|
26fc097672 | ||
|
|
0f2e2406e8 | ||
|
|
061a096600 | ||
|
|
07fe963bed | ||
|
|
7a1eda98ca | ||
|
|
e8a5e44319 | ||
|
|
486319407a | ||
|
|
4f3d917c9b | ||
|
|
e2761965d5 | ||
|
|
171ab196c8 | ||
|
|
5630cea9a0 | ||
|
|
5a4189a770 | ||
|
|
2440d9fc48 | ||
|
|
bd33aebc3d | ||
|
|
a46d467b75 | ||
|
|
8c6981da0d | ||
|
|
500a9f647f | ||
|
|
71968fca0d | ||
|
|
fc6f84897f | ||
|
|
b31ad4b1f8 | ||
|
|
aababf37a8 | ||
|
|
44a42a177a | ||
|
|
e773a92e53 | ||
|
|
196d27b959 | ||
|
|
ec8861e983 | ||
|
|
9b601eb9fe | ||
|
|
c80d9cbf01 | ||
|
|
717c15256f | ||
|
|
13dafc2091 | ||
|
|
cfc3bcfa41 | ||
|
|
2e052e97c5 | ||
|
|
5d48d05287 | ||
|
|
e34ba844c5 | ||
|
|
1d84d3e440 | ||
|
|
7a7e9087e6 | ||
|
|
8481caa67c | ||
|
|
651e63a0fb | ||
|
|
88e462c4e5 | ||
|
|
8ef797d1a4 | ||
|
|
877fd1af35 | ||
|
|
fc01eecdcb | ||
|
|
e6ba6203bb | ||
|
|
6a786f18e6 | ||
|
|
57d46287d7 | ||
|
|
bb6ab74c19 | ||
|
|
3e759b3e8e | ||
|
|
47ee8d54a8 | ||
|
|
824b984a64 | ||
|
|
618185a52d | ||
|
|
8dbc6ab7d3 | ||
|
|
5eb6754b7c | ||
|
|
a914a92cea | ||
|
|
439a1a758e | ||
|
|
9383240627 | ||
|
|
ea765eb929 | ||
|
|
d5c816f24d | ||
|
|
cf5f20bbb5 | ||
|
|
27839dfd21 | ||
|
|
27517e1aee | ||
|
|
a31bfbefa7 | ||
|
|
f1729163cf | ||
|
|
6747d97d60 | ||
|
|
2fb2342fd4 | ||
|
|
0f530b7e80 | ||
|
|
78e3cf4ffb | ||
|
|
4e512b7023 | ||
|
|
ee3fed97e0 | ||
|
|
b45d4f6401 | ||
|
|
d4c28de2c4 | ||
|
|
b7d613ace6 | ||
|
|
68dbb31272 | ||
|
|
8fe1fa48b8 | ||
|
|
e5575b782c | ||
|
|
ea112dd93d | ||
|
|
056fc5e087 | ||
|
|
cacb02b7c8 | ||
|
|
c0b6ec8ccb | ||
|
|
f4c8f0a3d8 | ||
|
|
81407ffe3c | ||
|
|
6f7dbe99fe | ||
|
|
ec9afba029 | ||
|
|
6164c5d78d | ||
|
|
94dbcd5c06 | ||
|
|
4d0dee7ee8 | ||
|
|
49e10698b1 | ||
|
|
8e1536b59b | ||
|
|
aed160994a | ||
|
|
b2b3173420 | ||
|
|
f465af964a | ||
|
|
5a7d6a673a | ||
|
|
7fb9aa2115 | ||
|
|
fea303ac8b | ||
|
|
8e8291a0bd | ||
|
|
4dc175fcd2 | ||
|
|
009a99a8eb | ||
|
|
91caa67e04 | ||
|
|
53793f7dd2 | ||
|
|
32af8783ba | ||
|
|
d6f51f8eba | ||
|
|
f40b7361e8 | ||
|
|
0231e114d5 | ||
|
|
ca61a3e83f | ||
|
|
6979f5c82d | ||
|
|
7d45b45667 | ||
|
|
7575504b80 | ||
|
|
36bcb611f1 | ||
|
|
cc34c36794 | ||
|
|
b84cb93d2c | ||
|
|
08211a7740 | ||
|
|
97985dd458 | ||
|
|
edf157215c | ||
|
|
89671c71fa | ||
|
|
eeedcc50ab | ||
|
|
de0bb069aa | ||
|
|
5d53f8bfb3 | ||
|
|
d4a7de8070 | ||
|
|
3fe15641c7 | ||
|
|
08df71a7e4 | ||
|
|
77db25b4d9 | ||
|
|
62bb5a28aa | ||
|
|
f768b200d2 | ||
|
|
a5b6db74e5 | ||
|
|
cbe4467a22 | ||
|
|
8417d18eec | ||
|
|
93ae8187cd | ||
|
|
972338d86a | ||
|
|
a4a6e3b8f6 | ||
|
|
d788dd3042 | ||
|
|
974760f020 | ||
|
|
65569c4a60 | ||
|
|
39da45f0bd | ||
|
|
dc7c27e619 | ||
|
|
90661b7743 | ||
|
|
d07571ea7e | ||
|
|
fbf29e336f | ||
|
|
09a0a803a1 | ||
|
|
dd0327ba85 | ||
|
|
d900887d97 | ||
|
|
2a0efe70a5 | ||
|
|
ce09e57315 | ||
|
|
91023c5239 | ||
|
|
714fb10fac | ||
|
|
aa3e43207f | ||
|
|
e045989d39 | ||
|
|
ad5695210d | ||
|
|
4dab6e5008 | ||
|
|
522f26cf4e | ||
|
|
3eca4bc439 | ||
|
|
b374f67b52 | ||
|
|
c68277be2c | ||
|
|
bb8a3e82e6 | ||
|
|
b511d4ba53 | ||
|
|
fd997d4bda | ||
|
|
21d04a9f85 | ||
|
|
dcb5c12197 | ||
|
|
125e8766c5 | ||
|
|
7bce1f6df4 | ||
|
|
8f66fb6fef | ||
|
|
711e009e6d | ||
|
|
212fe31cb3 | ||
|
|
a3dc4fa001 | ||
|
|
ff53113358 | ||
|
|
94cbe15b54 | ||
|
|
90516e035d | ||
|
|
c77c754f6d | ||
|
|
288c52a2cd | ||
|
|
f0fa7326dd | ||
|
|
f29b0f2f26 | ||
|
|
5dcac24f99 | ||
|
|
1eb11ac377 | ||
|
|
2e89cdcfad | ||
|
|
1b0beccd3d | ||
|
|
abb5a72fc4 | ||
|
|
bf706f7586 | ||
|
|
4459663510 | ||
|
|
68a34dc28b | ||
|
|
ba1826587c | ||
|
|
2e7a8d50b0 | ||
|
|
2a4ca21050 | ||
|
|
0ed6c57c7f | ||
|
|
983d44947e | ||
|
|
5bd88dc82f | ||
|
|
51bbab803f | ||
|
|
a587ba4558 | ||
|
|
075b6cca30 | ||
|
|
4c263bbb3e | ||
|
|
420627d892 | ||
|
|
ce3a409997 | ||
|
|
0f9f1782ae | ||
|
|
d4704a02c5 | ||
|
|
9ca2ed2c93 | ||
|
|
ae12ffdefb | ||
|
|
1e13097126 | ||
|
|
9b8a6b1168 | ||
|
|
8f6dda5cd3 | ||
|
|
91780f236e | ||
|
|
f16474815c | ||
|
|
a8ff6d7e6e | ||
|
|
8395782a2e | ||
|
|
28ea625b01 | ||
|
|
efc569f6ed | ||
|
|
d377e10771 | ||
|
|
fef85877e6 | ||
|
|
19135bfaf4 | ||
|
|
5a147fccc2 | ||
|
|
20976814ba | ||
|
|
27e2612fc9 | ||
|
|
3ea432ef2d | ||
|
|
fe6acbc1e3 | ||
|
|
379b162e64 | ||
|
|
84832955dd | ||
|
|
34b99a51c3 | ||
|
|
fb5245e2af | ||
|
|
ff0a830e0c | ||
|
|
a365f071a4 | ||
|
|
f290e7b5b2 | ||
|
|
83a0ae673c | ||
|
|
7de646c290 | ||
|
|
5d681a72ac | ||
|
|
d725970e6e | ||
|
|
54b82760cd | ||
|
|
77f79fa913 | ||
|
|
6db714a2b1 | ||
|
|
913ac7c9f5 | ||
|
|
eecbfd29e7 | ||
|
|
41025225b2 | ||
|
|
07c7b28c03 | ||
|
|
3faf3d2a56 | ||
|
|
be094d5b7c | ||
|
|
459673bd59 | ||
|
|
2968b70e6b | ||
|
|
b6989a18fc | ||
|
|
4d6a504836 | ||
|
|
6c3737df68 | ||
|
|
9f382ed9bd | ||
|
|
f9e86e58d6 | ||
|
|
469f9c927f | ||
|
|
312724189b | ||
|
|
ec0a1f1ce4 | ||
|
|
ad93631ef8 | ||
|
|
3b6238ff93 | ||
|
|
5b9facee00 | ||
|
|
376f2a554a | ||
|
|
5fd0bd3db9 | ||
|
|
2deb3bbf23 | ||
|
|
01bef0c2b8 | ||
|
|
b65a8ce680 | ||
|
|
5fc4f63238 | ||
|
|
9b0e0c175b | ||
|
|
eb97e925e6 | ||
|
|
16f8975b18 | ||
|
|
5073fce2f7 | ||
|
|
cf5036eec6 | ||
|
|
20be37dd8b | ||
|
|
93349ae2dc | ||
|
|
be81de74cd | ||
|
|
88e739090d | ||
|
|
7f324f1957 | ||
|
|
4f31c9e72f | ||
|
|
c9a663ed39 | ||
|
|
105d8aeb6b | ||
|
|
6ea1211bc5 | ||
|
|
c13291a598 | ||
|
|
aaa78c618c | ||
|
|
2890c677d4 | ||
|
|
51a010de00 | ||
|
|
ca0aabd814 | ||
|
|
91d1ba1910 | ||
|
|
c7c739dc00 | ||
|
|
7a8389bf11 | ||
|
|
5f062b8ea3 | ||
|
|
b9ee14a0f9 | ||
|
|
c3baf73455 |
@@ -3,16 +3,24 @@ root = true
|
|||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
insert_final_newline = false
|
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = false
|
||||||
|
|
||||||
[*.{json,js,css}]
|
[*.{json,js,css}]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.{md,rs}]
|
[*.md]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.{rs,toml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.snap]
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.lua]
|
[*.lua]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
36
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
rust_version: [stable, "1.40.0"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain
|
||||||
|
run: rustup default ${{ matrix.rust_version }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --locked --verbose
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --locked --verbose
|
||||||
|
|
||||||
|
- name: Rustfmt and Clippy
|
||||||
|
run: |
|
||||||
|
cargo fmt -- --check
|
||||||
|
cargo clippy
|
||||||
|
if: matrix.rust_version == 'stable'
|
||||||
|
|
||||||
|
- name: Build (All Features)
|
||||||
|
run: cargo build --locked --verbose --all-features
|
||||||
|
|
||||||
|
- name: Run tests (All Features)
|
||||||
|
run: cargo test --locked --verbose --all-features
|
||||||
60
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ["*"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Build release binary
|
||||||
|
run: cargo build --verbose --locked --release
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: rojo-win64
|
||||||
|
path: target/release/rojo.exe
|
||||||
|
|
||||||
|
macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
|
||||||
|
- name: Build release binary
|
||||||
|
run: |
|
||||||
|
source $HOME/.cargo/env
|
||||||
|
cargo build --verbose --locked --release
|
||||||
|
env:
|
||||||
|
OPENSSL_STATIC: 1
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: rojo-macos
|
||||||
|
path: target/release/rojo
|
||||||
|
|
||||||
|
linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --locked --verbose --release
|
||||||
|
env:
|
||||||
|
OPENSSL_STATIC: 1
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: rojo-linux
|
||||||
|
path: target/release/rojo
|
||||||
21
.gitignore
vendored
@@ -1,5 +1,18 @@
|
|||||||
/site
|
# Rust output directory
|
||||||
/target
|
/target
|
||||||
/scratch-project
|
|
||||||
**/*.rs.bk
|
# Headers for clibrojo
|
||||||
/generate-docs.run
|
/include
|
||||||
|
|
||||||
|
# Roblox model and place files in the root, used for debugging
|
||||||
|
/*.rbxm
|
||||||
|
/*.rbxmx
|
||||||
|
/*.rbxl
|
||||||
|
/*.rbxlx
|
||||||
|
|
||||||
|
# Roblox Studio holds 'lock' files on places
|
||||||
|
*.rbxl.lock
|
||||||
|
*.rbxlx.lock
|
||||||
|
|
||||||
|
# Snapshot files from the 'insta' Rust crate
|
||||||
|
**/*.snap.new
|
||||||
15
.gitmodules
vendored
@@ -1,18 +1,15 @@
|
|||||||
[submodule "plugin/modules/roact"]
|
[submodule "plugin/modules/roact"]
|
||||||
path = plugin/modules/roact
|
path = plugin/modules/roact
|
||||||
url = https://github.com/Roblox/roact.git
|
url = https://github.com/Roblox/roact.git
|
||||||
[submodule "plugin/modules/rodux"]
|
|
||||||
path = plugin/modules/rodux
|
|
||||||
url = https://github.com/Roblox/rodux.git
|
|
||||||
[submodule "plugin/modules/roact-rodux"]
|
|
||||||
path = plugin/modules/roact-rodux
|
|
||||||
url = https://github.com/Roblox/roact-rodux.git
|
|
||||||
[submodule "plugin/modules/testez"]
|
[submodule "plugin/modules/testez"]
|
||||||
path = plugin/modules/testez
|
path = plugin/modules/testez
|
||||||
url = https://github.com/Roblox/testez.git
|
url = https://github.com/Roblox/testez.git
|
||||||
[submodule "plugin/modules/lemur"]
|
|
||||||
path = plugin/modules/lemur
|
|
||||||
url = https://github.com/LPGhatguy/lemur.git
|
|
||||||
[submodule "plugin/modules/promise"]
|
[submodule "plugin/modules/promise"]
|
||||||
path = plugin/modules/promise
|
path = plugin/modules/promise
|
||||||
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
url = https://github.com/LPGhatguy/roblox-lua-promise.git
|
||||||
|
[submodule "plugin/modules/t"]
|
||||||
|
path = plugin/modules/t
|
||||||
|
url = https://github.com/osyrisrblx/t.git
|
||||||
|
[submodule "plugin/modules/rbx-dom"]
|
||||||
|
path = plugin/modules/rbx-dom
|
||||||
|
url = http://github.com/rojo-rbx/rbx-dom
|
||||||
@@ -13,12 +13,14 @@ stds.roblox = {
|
|||||||
|
|
||||||
-- Types
|
-- Types
|
||||||
"Vector2", "Vector3",
|
"Vector2", "Vector3",
|
||||||
|
"Vector2int16", "Vector3int16",
|
||||||
"Color3",
|
"Color3",
|
||||||
"UDim", "UDim2",
|
"UDim", "UDim2",
|
||||||
"Rect",
|
"Rect",
|
||||||
"CFrame",
|
"CFrame",
|
||||||
"Enum",
|
"Enum",
|
||||||
"Instance",
|
"Instance",
|
||||||
|
"DockWidgetPluginGuiInfo",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
47
.travis.yml
@@ -1,47 +0,0 @@
|
|||||||
matrix:
|
|
||||||
include:
|
|
||||||
- language: python
|
|
||||||
env:
|
|
||||||
- LUA="lua=5.1"
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- pip install hererocks
|
|
||||||
- hererocks lua_install -r^ --$LUA
|
|
||||||
- export PATH=$PATH:$PWD/lua_install/bin
|
|
||||||
|
|
||||||
install:
|
|
||||||
- luarocks install luafilesystem
|
|
||||||
- luarocks install busted
|
|
||||||
- luarocks install luacov
|
|
||||||
- luarocks install luacov-coveralls
|
|
||||||
- luarocks install luacheck
|
|
||||||
|
|
||||||
script:
|
|
||||||
- cd plugin
|
|
||||||
- luacheck src
|
|
||||||
- lua -lluacov spec.lua
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- cd plugin
|
|
||||||
- luacov-coveralls -e $TRAVIS_BUILD_DIR/lua_install
|
|
||||||
|
|
||||||
- language: rust
|
|
||||||
rust: 1.31.1
|
|
||||||
cache: cargo
|
|
||||||
|
|
||||||
script:
|
|
||||||
- cargo test --verbose
|
|
||||||
|
|
||||||
- language: rust
|
|
||||||
rust: stable
|
|
||||||
cache: cargo
|
|
||||||
|
|
||||||
script:
|
|
||||||
- cargo test --verbose
|
|
||||||
|
|
||||||
- language: rust
|
|
||||||
rust: beta
|
|
||||||
cache: cargo
|
|
||||||
|
|
||||||
script:
|
|
||||||
- cargo test --verbose
|
|
||||||
232
CHANGELOG.md
@@ -1,29 +1,165 @@
|
|||||||
# Rojo Changelog
|
# Rojo Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## Unreleased Changes for 0.6.x
|
||||||
|
|
||||||
## [0.5.0 Alpha 4](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.4) (February 8, 2019)
|
## [0.6.0 Alpha 2](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.2) (March 6, 2020)
|
||||||
* Added support for nested partitions ([#102](https://github.com/LPGhatguy/rojo/issues/102))
|
* Fixed `rojo upload` command always uploading models.
|
||||||
* Added support for 'transmuting' partitions ([#112](https://github.com/LPGhatguy/rojo/issues/112))
|
* Removed `--kind` parameter to `rojo upload`; Rojo now automatically uploads the correct kind of asset based on your project file.
|
||||||
* Added support for aliasing filesystem paths ([#105](https://github.com/LPGhatguy/rojo/issues/105))
|
|
||||||
* Changed Windows builds to statically link the CRT ([#89](https://github.com/LPGhatguy/rojo/issues/89))
|
|
||||||
|
|
||||||
## [0.5.0 Alpha 3](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.3) (February 1, 2019)
|
## [0.5.4](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.4) (February 26, 2020)
|
||||||
* Changed default project file name from `roblox-project.json` to `default.project.json` ([#120](https://github.com/LPGhatguy/rojo/pull/120))
|
This is a general maintenance release for the Rojo 0.5.x release series.
|
||||||
|
|
||||||
|
* Updated reflection database and other dependencies.
|
||||||
|
* First stable release with binaries for macOS and Linux.
|
||||||
|
|
||||||
|
## [0.6.0 Alpha 1](https://github.com/rojo-rbx/rojo/releases/tag/v0.6.0-alpha.1) (January 22, 2020)
|
||||||
|
|
||||||
|
### General
|
||||||
|
* Added support for nested project files. ([#95](https://github.com/rojo-rbx/rojo/issues/95))
|
||||||
|
* Added project file hot-reloading. ([#10](https://github.com/rojo-rbx/rojo/issues/10)])
|
||||||
|
* Fixed Rojo dropping Ref properties ([#142](https://github.com/rojo-rbx/rojo/issues/142))
|
||||||
|
* This means that properties like `PrimaryPart` now work!
|
||||||
|
* Improved live sync protocol to reduce round-trips and improve syncing consistency.
|
||||||
|
* Improved support for binary model files and places.
|
||||||
|
|
||||||
|
### Command Line
|
||||||
|
* Added `--verbose`/`-v` flag, which can be specified multiple times to increase verbosity.
|
||||||
|
* Added support for automatically finding Roblox Studio's auth cookie for `rojo upload` on Windows.
|
||||||
|
* Added support for building, serving and uploading sources that aren't Rojo projects.
|
||||||
|
* Improved feedback from `rojo serve`.
|
||||||
|
* Removed support for legacy `roblox-project.json` projects, deprecated in an early Rojo 0.5.0 alpha.
|
||||||
|
* Rojo no longer traverses directories upwards looking for project files.
|
||||||
|
* Though undocumented, Rojo 0.5.x will search for a project file contained in any ancestor folders. This feature was removed to better support other 0.6.x features.
|
||||||
|
|
||||||
|
### Roblox Studio Plugin
|
||||||
|
* Added "connecting" state to improve experience when live syncing.
|
||||||
|
* Added "error" state to show errors in a place that isn't the output panel.
|
||||||
|
* Improved diagnostics for when the Rojo plugin cannot create an instance.
|
||||||
|
|
||||||
|
## [0.5.3](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.3) (October 15, 2019)
|
||||||
|
* Fixed an issue where Rojo would throw an error when encountering recently-added instance classes.
|
||||||
|
|
||||||
|
## [0.5.2](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.2) (October 14, 2019)
|
||||||
|
* Fixed an issue where `LocalizationTable` instances would have their column order randomized. ([#173](https://github.com/rojo-rbx/rojo/issues/173))
|
||||||
|
|
||||||
|
## [0.5.1](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.1) (October 4, 2019)
|
||||||
|
* Fixed an issue where Rojo would drop changes if they happened too quickly ([#252](https://github.com/rojo-rbx/rojo/issues/252))
|
||||||
|
* Improved diagnostics for when the Rojo plugin cannot create an instance.
|
||||||
|
* Updated dependencies
|
||||||
|
* This brings Rojo's reflection database from client release 395 to client release 404.
|
||||||
|
|
||||||
|
## [0.5.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0) (August 27, 2019)
|
||||||
|
* Changed `.model.json` naming, which may require projects to migrate ambiguous cases:
|
||||||
|
* The file name now takes precedence over the `Name` field in the model, like Rojo 0.4.x.
|
||||||
|
* The `Name` field of the top-level instance is now optional. It's recommended that you remove it from your models.
|
||||||
|
* Rojo will emit a warning when `Name` is specified and does not match the name from the file.
|
||||||
|
* Fixed `Rect` values being set to `0, 0, 0, 0` when synced with the Rojo plugin. ([#201](https://github.com/rojo-rbx/rojo/issues/201))
|
||||||
|
* Fixed live-syncing of `PhysicalProperties`, `NumberSequence`, and `ColorSequence` values
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 13](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.13) (August 2, 2019)
|
||||||
|
* Bumped minimum Rust version to 1.34.0.
|
||||||
|
* Fixed default port documentation in `rojo serve --help` ([#219](https://github.com/rojo-rbx/rojo/issues/219))
|
||||||
|
* Fixed BrickColor support by upgrading Roblox-related dependencies
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 12](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.12) (July 2, 2019)
|
||||||
|
* Added `.meta.json` files
|
||||||
|
* `init.meta.json` files replace `init.model.json` files from Rojo 0.4.x ([#183](https://github.com/rojo-rbx/rojo/pull/183))
|
||||||
|
* Other `.meta.json` files allow attaching extra data to other files ([#189](https://github.com/rojo-rbx/rojo/pull/189))
|
||||||
|
* Added support for infinite and NaN values in types like `Vector2` when building models and places.
|
||||||
|
* These types aren't supported for live-syncing yet due to limitations around JSON encoding.
|
||||||
|
* Added support for using `SharedString` values when building XML models and places.
|
||||||
|
* Added support for live-syncing `CollectionService` tags.
|
||||||
|
* Added a warning when building binary place files, since they're still experimental and have bugs.
|
||||||
|
* Added a warning when trying to use Rojo 0.5.x with a Rojo 0.4.x-only project.
|
||||||
|
* Added a warning when a Rojo project contains keys that start with `$`, which are reserved names. ([#191](https://github.com/rojo-rbx/rojo/issues/191))
|
||||||
|
* Rojo now throws an error if unknown keys are found most files.
|
||||||
|
* Added an icon to the plugin's toolbar button
|
||||||
|
* Changed the plugin to use a docking widget for all UI.
|
||||||
|
* Changed the plugin to ignore unknown properties when live-syncing.
|
||||||
|
* Rojo's approach to this problem might change later, like with a strict model mode ([#190](https://github.com/rojo-rbx/rojo/issues/190)) or another approach.
|
||||||
|
* Upgraded to reflection database from client release 388.
|
||||||
|
* Updated Rojo's branding to shift the color palette to make it work better on dark backgrounds
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 11](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.11) (May 29, 2019)
|
||||||
|
* Added support for implicit property values in JSON model files ([#154](https://github.com/rojo-rbx/rojo/pull/154))
|
||||||
|
* `Content` propertyes can now be specified in projects and model files as regular string literals.
|
||||||
|
* Added support for `BrickColor` properties.
|
||||||
|
* Added support for properties added in client release 384, like `Lighting.Technology` being set to `"ShadowMap"`.
|
||||||
|
* Improved performance when working with XML models and places
|
||||||
|
* Fixed serializing empty `Content` properties as XML
|
||||||
|
* Fixed serializing infinite and NaN floating point properties in XML
|
||||||
|
* Improved compatibility with XML models
|
||||||
|
* Plugin should now be able to live-sync more properties, and ignore ones it can't, like `Lighting.Technology`.
|
||||||
|
|
||||||
|
## 0.5.0 Alpha 10
|
||||||
|
* This release was a dud due to [issue #176](https://github.com/rojo-rbx/rojo/issues/176) and was rolled back.
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 9](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.9) (April 4, 2019)
|
||||||
|
* Changed `rojo build` to use buffered I/O, which can make it up to 2x faster in some cases.
|
||||||
|
* Building [*Road Not Taken*](https://github.com/rojo-rbx/roads) to an `rbxlx` file dropped from 150ms to 70ms on my machine
|
||||||
|
* Fixed `LocalizationTable` instances being made from `csv` files incorrectly interpreting empty rows and columns. ([#149](https://github.com/rojo-rbx/rojo/pull/149))
|
||||||
|
* Fixed CSV files with entries that parse as numbers causing Rojo to panic. ([#152](https://github.com/rojo-rbx/rojo/pull/152))
|
||||||
|
* Improved error messages when malformed CSV files are found in a Rojo project.
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 8](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.8) (March 29, 2019)
|
||||||
|
* Added support for a bunch of new types when dealing with XML model/place files:
|
||||||
|
* `ColorSequence`
|
||||||
|
* `Float64`
|
||||||
|
* `Int64`
|
||||||
|
* `NumberRange`
|
||||||
|
* `NumberSequence`
|
||||||
|
* `PhysicalProperties`
|
||||||
|
* `Ray`
|
||||||
|
* `Rect`
|
||||||
|
* `Ref`
|
||||||
|
* Improved server instance ordering behavior when files are added during a live session ([#135](https://github.com/rojo-rbx/rojo/pull/135))
|
||||||
|
* Fixed error being thrown when trying to unload the Rojo plugin.
|
||||||
|
* Added partial fix for [issue #141](https://github.com/rojo-rbx/rojo/issues/141) for `Lighting.Technology`, which should restore live sync functionality for the default project file.
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 6](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.6) (March 19, 2019)
|
||||||
|
* Fixed `rojo init` giving unexpected results by upgrading to `rbx_dom_weak` 1.1.0
|
||||||
|
* Fixed live server not responding when the Rojo plugin is connected ([#133](https://github.com/rojo-rbx/rojo/issues/133))
|
||||||
|
* Updated default place file:
|
||||||
|
* Improved default properties to be closer to Studio's built-in 'Baseplate' template
|
||||||
|
* Added a baseplate to the project file (Thanks, [@AmaranthineCodices](https://github.com/AmaranthineCodices/)!)
|
||||||
|
* Added more type support to Rojo plugin
|
||||||
|
* Fixed some cases where the Rojo plugin would leave around objects that it knows should be deleted
|
||||||
|
* Updated plugin to correctly listen to `Plugin.Unloading` when installing or uninstalling new plugins
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 5](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.5) (March 1, 2019)
|
||||||
|
* Upgraded core dependencies, which improves compatibility for lots of instance types
|
||||||
|
* Upgraded from `rbx_tree` 0.2.0 to `rbx_dom_weak` 1.0.0
|
||||||
|
* Upgraded from `rbx_xml` 0.2.0 to `rbx_xml` 0.4.0
|
||||||
|
* Upgraded from `rbx_binary` 0.2.0 to `rbx_binary` 0.4.0
|
||||||
|
* Added support for non-primitive types in the Rojo plugin.
|
||||||
|
* Types like `Color3` and `CFrame` can now be updated live!
|
||||||
|
* Fixed plugin assets flashing in on first load ([#121](https://github.com/rojo-rbx/rojo/issues/121))
|
||||||
|
* Changed Rojo's HTTP server from Rouille to Hyper, which reduced the release size by around a megabyte.
|
||||||
|
* Added property type inference to projects, which makes specifying services a lot easier ([#130](https://github.com/rojo-rbx/rojo/pull/130))
|
||||||
|
* Made error messages from invalid and missing files more user-friendly
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 4](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.4) (February 8, 2019)
|
||||||
|
* Added support for nested partitions ([#102](https://github.com/rojo-rbx/rojo/issues/102))
|
||||||
|
* Added support for 'transmuting' partitions ([#112](https://github.com/rojo-rbx/rojo/issues/112))
|
||||||
|
* Added support for aliasing filesystem paths ([#105](https://github.com/rojo-rbx/rojo/issues/105))
|
||||||
|
* Changed Windows builds to statically link the CRT ([#89](https://github.com/rojo-rbx/rojo/issues/89))
|
||||||
|
|
||||||
|
## [0.5.0 Alpha 3](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.3) (February 1, 2019)
|
||||||
|
* Changed default project file name from `roblox-project.json` to `default.project.json` ([#120](https://github.com/rojo-rbx/rojo/pull/120))
|
||||||
* The old file name will still be supported until 0.5.0 is fully released.
|
* The old file name will still be supported until 0.5.0 is fully released.
|
||||||
* Added warning when loading project files that don't end in `.project.json`
|
* Added warning when loading project files that don't end in `.project.json`
|
||||||
* This new extension enables Rojo to distinguish project files from random JSON files, which is necessary to support nested projects.
|
* This new extension enables Rojo to distinguish project files from random JSON files, which is necessary to support nested projects.
|
||||||
* Added new (empty) diagnostic page served from the server
|
* Added new (empty) diagnostic page served from the server
|
||||||
* Added better error messages for when a file is missing that's referenced by a Rojo project
|
* Added better error messages for when a file is missing that's referenced by a Rojo project
|
||||||
* Added support for visualization endpoints returning GraphViz source when Dot is not available
|
* Added support for visualization endpoints returning GraphViz source when Dot is not available
|
||||||
* Fixed an in-memory filesystem regression introduced recently ([#119](https://github.com/LPGhatguy/rojo/pull/119))
|
* Fixed an in-memory filesystem regression introduced recently ([#119](https://github.com/rojo-rbx/rojo/pull/119))
|
||||||
|
|
||||||
## [0.5.0 Alpha 2](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.2) (January 28, 2019)
|
## [0.5.0 Alpha 2](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.2) (January 28, 2019)
|
||||||
* Added support for `.model.json` files, compatible with 0.4.x
|
* Added support for `.model.json` files, compatible with 0.4.x
|
||||||
* Fixed in-memory filesystem not handling out-of-order filesystem change events
|
* Fixed in-memory filesystem not handling out-of-order filesystem change events
|
||||||
* Fixed long-polling error caused by a promise mixup ([#110](https://github.com/LPGhatguy/rojo/issues/110))
|
* Fixed long-polling error caused by a promise mixup ([#110](https://github.com/rojo-rbx/rojo/issues/110))
|
||||||
|
|
||||||
## [0.5.0 Alpha 1](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.1) (January 25, 2019)
|
## [0.5.0 Alpha 1](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.1) (January 25, 2019)
|
||||||
* Changed plugin UI to be way prettier
|
* Changed plugin UI to be way prettier
|
||||||
* Thanks to [Reselim](https://github.com/Reselim) for the design!
|
* Thanks to [Reselim](https://github.com/Reselim) for the design!
|
||||||
* Changed plugin error messages to be a little more useful
|
* Changed plugin error messages to be a little more useful
|
||||||
@@ -31,7 +167,7 @@
|
|||||||
* Fixed bug where bad server responses could cause the plugin to be in a bad state
|
* Fixed bug where bad server responses could cause the plugin to be in a bad state
|
||||||
* Upgraded to rbx\_tree, rbx\_xml, and rbx\_binary 0.2.0, which dramatically expands the kinds of properties that Rojo can handle, especially in XML.
|
* Upgraded to rbx\_tree, rbx\_xml, and rbx\_binary 0.2.0, which dramatically expands the kinds of properties that Rojo can handle, especially in XML.
|
||||||
|
|
||||||
## [0.5.0 Alpha 0](https://github.com/LPGhatguy/rojo/releases/tag/v0.5.0-alpha.0) (January 14, 2019)
|
## [0.5.0 Alpha 0](https://github.com/rojo-rbx/rojo/releases/tag/v0.5.0-alpha.0) (January 14, 2019)
|
||||||
* "Epiphany" rewrite, in progress since the beginning of time
|
* "Epiphany" rewrite, in progress since the beginning of time
|
||||||
* New live sync protocol
|
* New live sync protocol
|
||||||
* Uses HTTP long polling to reduce request count and improve responsiveness
|
* Uses HTTP long polling to reduce request count and improve responsiveness
|
||||||
@@ -56,105 +192,105 @@
|
|||||||
* Multiple places can be specified, like when building a multi-place game
|
* Multiple places can be specified, like when building a multi-place game
|
||||||
* Added support for specifying properties on services in project files
|
* Added support for specifying properties on services in project files
|
||||||
|
|
||||||
## [0.4.13](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.13) (November 12, 2018)
|
## [0.4.13](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.13) (November 12, 2018)
|
||||||
* When `rojo.json` points to a file or directory that does not exist, Rojo now issues a warning instead of throwing an error and exiting
|
* When `rojo.json` points to a file or directory that does not exist, Rojo now issues a warning instead of throwing an error and exiting
|
||||||
|
|
||||||
## [0.4.12](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.12) (June 21, 2018)
|
## [0.4.12](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.12) (June 21, 2018)
|
||||||
* Fixed obscure assertion failure when renaming or deleting files ([#78](https://github.com/LPGhatguy/rojo/issues/78))
|
* Fixed obscure assertion failure when renaming or deleting files ([#78](https://github.com/rojo-rbx/rojo/issues/78))
|
||||||
* Added a `PluginAction` for the sync in command, which should help with some automation scripts ([#80](https://github.com/LPGhatguy/rojo/pull/80))
|
* Added a `PluginAction` for the sync in command, which should help with some automation scripts ([#80](https://github.com/rojo-rbx/rojo/pull/80))
|
||||||
|
|
||||||
## [0.4.11](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.11) (June 10, 2018)
|
## [0.4.11](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.11) (June 10, 2018)
|
||||||
* Defensively insert existing instances into RouteMap; should fix most duplication cases when syncing into existing trees.
|
* Defensively insert existing instances into RouteMap; should fix most duplication cases when syncing into existing trees.
|
||||||
* Fixed incorrect synchronization from `Plugin:_pull` that would cause polling to create issues
|
* Fixed incorrect synchronization from `Plugin:_pull` that would cause polling to create issues
|
||||||
* Fixed incorrect file routes being assigned to `init.lua` and `init.model.json` files
|
* Fixed incorrect file routes being assigned to `init.lua` and `init.model.json` files
|
||||||
* Untangled route handling-internals slightly
|
* Untangled route handling-internals slightly
|
||||||
|
|
||||||
## [0.4.10](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.10) (June 2, 2018)
|
## [0.4.10](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.10) (June 2, 2018)
|
||||||
* Added support for `init.model.json` files, which enable versioning `Tool` instances (among other things) with Rojo. ([#66](https://github.com/LPGhatguy/rojo/issues/66))
|
* Added support for `init.model.json` files, which enable versioning `Tool` instances (among other things) with Rojo. ([#66](https://github.com/rojo-rbx/rojo/issues/66))
|
||||||
* Fixed obscure error when syncing into an invalid service.
|
* Fixed obscure error when syncing into an invalid service.
|
||||||
* Fixed multiple sync processes occurring when a server ID mismatch is detected.
|
* Fixed multiple sync processes occurring when a server ID mismatch is detected.
|
||||||
|
|
||||||
## [0.4.9](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.9) (May 26, 2018)
|
## [0.4.9](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.9) (May 26, 2018)
|
||||||
* Fixed warning when renaming or removing files that would sometimes corrupt the instance cache ([#72](https://github.com/LPGhatguy/rojo/pull/72))
|
* Fixed warning when renaming or removing files that would sometimes corrupt the instance cache ([#72](https://github.com/rojo-rbx/rojo/pull/72))
|
||||||
* JSON models are no longer as strict -- `Children` and `Properties` are now optional.
|
* JSON models are no longer as strict -- `Children` and `Properties` are now optional.
|
||||||
|
|
||||||
## [0.4.8](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.8) (May 26, 2018)
|
## [0.4.8](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.8) (May 26, 2018)
|
||||||
* Hotfix to prevent errors from being thrown when objects managed by Rojo are deleted
|
* Hotfix to prevent errors from being thrown when objects managed by Rojo are deleted
|
||||||
|
|
||||||
## [0.4.7](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.7) (May 25, 2018)
|
## [0.4.7](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.7) (May 25, 2018)
|
||||||
* Added icons to the Rojo plugin, made by [@Vorlias](https://github.com/Vorlias)! ([#70](https://github.com/LPGhatguy/rojo/pull/70))
|
* Added icons to the Rojo plugin, made by [@Vorlias](https://github.com/Vorlias)! ([#70](https://github.com/rojo-rbx/rojo/pull/70))
|
||||||
* Server will now issue a warning if no partitions are specified in `rojo serve` ([#40](https://github.com/LPGhatguy/rojo/issues/40))
|
* Server will now issue a warning if no partitions are specified in `rojo serve` ([#40](https://github.com/rojo-rbx/rojo/issues/40))
|
||||||
|
|
||||||
## [0.4.6](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.6) (May 21, 2018)
|
## [0.4.6](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.6) (May 21, 2018)
|
||||||
* Rojo handles being restarted by Roblox Studio more gracefully ([#67](https://github.com/LPGhatguy/rojo/issues/67))
|
* Rojo handles being restarted by Roblox Studio more gracefully ([#67](https://github.com/rojo-rbx/rojo/issues/67))
|
||||||
* Folders should no longer get collapsed when syncing occurs.
|
* Folders should no longer get collapsed when syncing occurs.
|
||||||
* **Significant** robustness improvements with regards to caching.
|
* **Significant** robustness improvements with regards to caching.
|
||||||
* **This should catch all existing script duplication bugs.**
|
* **This should catch all existing script duplication bugs.**
|
||||||
* If there are any bugs with script duplication or caching in the future, restarting the Rojo server process will fix them for that session.
|
* If there are any bugs with script duplication or caching in the future, restarting the Rojo server process will fix them for that session.
|
||||||
* Fixed message in plugin not being prefixed with `Rojo: `.
|
* Fixed message in plugin not being prefixed with `Rojo: `.
|
||||||
|
|
||||||
## [0.4.5](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.5) (May 1, 2018)
|
## [0.4.5](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.5) (May 1, 2018)
|
||||||
* Rojo messages are now prefixed with `Rojo: ` to make them stand out in the output more.
|
* Rojo messages are now prefixed with `Rojo: ` to make them stand out in the output more.
|
||||||
* Fixed server to notice file changes *much* more quickly. (200ms vs 1000ms)
|
* Fixed server to notice file changes *much* more quickly. (200ms vs 1000ms)
|
||||||
* Server now lists name of project when starting up.
|
* Server now lists name of project when starting up.
|
||||||
* Rojo now throws an error if no project file is found. ([#63](https://github.com/LPGhatguy/rojo/issues/63))
|
* Rojo now throws an error if no project file is found. ([#63](https://github.com/rojo-rbx/rojo/issues/63))
|
||||||
* Fixed multiple sync operations occuring at the same time. ([#61](https://github.com/LPGhatguy/rojo/issues/61))
|
* Fixed multiple sync operations occuring at the same time. ([#61](https://github.com/rojo-rbx/rojo/issues/61))
|
||||||
* Partitions targeting files directly now work as expected. ([#57](https://github.com/LPGhatguy/rojo/issues/57))
|
* Partitions targeting files directly now work as expected. ([#57](https://github.com/rojo-rbx/rojo/issues/57))
|
||||||
|
|
||||||
## [0.4.4](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.4) (April 7, 2018)
|
## [0.4.4](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.4) (April 7, 2018)
|
||||||
* Fix small regression introduced in 0.4.3
|
* Fix small regression introduced in 0.4.3
|
||||||
|
|
||||||
## [0.4.3](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.3) (April 7, 2018)
|
## [0.4.3](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.3) (April 7, 2018)
|
||||||
* Plugin now automatically selects `HttpService` if it determines that HTTP isn't enabled ([#58](https://github.com/LPGhatguy/rojo/pull/58))
|
* Plugin now automatically selects `HttpService` if it determines that HTTP isn't enabled ([#58](https://github.com/rojo-rbx/rojo/pull/58))
|
||||||
* Plugin now has much more robust handling and will wipe all state when the server changes.
|
* Plugin now has much more robust handling and will wipe all state when the server changes.
|
||||||
* This should fix issues that would otherwise be solved by restarting Roblox Studio.
|
* This should fix issues that would otherwise be solved by restarting Roblox Studio.
|
||||||
|
|
||||||
## [0.4.2](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.2) (April 4, 2018)
|
## [0.4.2](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.2) (April 4, 2018)
|
||||||
* Fixed final case of duplicated instance insertion, caused by reconciled instances not being inserted into `RouteMap`.
|
* Fixed final case of duplicated instance insertion, caused by reconciled instances not being inserted into `RouteMap`.
|
||||||
* The reconciler is still not a perfect solution, especially if script instances get moved around without being destroyed. I don't think this can be fixed before a big refactor.
|
* The reconciler is still not a perfect solution, especially if script instances get moved around without being destroyed. I don't think this can be fixed before a big refactor.
|
||||||
|
|
||||||
## [0.4.1](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.1) (April 1, 2018)
|
## [0.4.1](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.1) (April 1, 2018)
|
||||||
* Merged plugin repository into main Rojo repository for easier tracking.
|
* Merged plugin repository into main Rojo repository for easier tracking.
|
||||||
* Improved `RouteMap` object tracking; this should fix some cases of duplicated instances being synced into the tree.
|
* Improved `RouteMap` object tracking; this should fix some cases of duplicated instances being synced into the tree.
|
||||||
|
|
||||||
## [0.4.0](https://github.com/LPGhatguy/rojo/releases/tag/v0.4.0) (March 27, 2018)
|
## [0.4.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.4.0) (March 27, 2018)
|
||||||
* Protocol version 1, which shifts more responsibility onto the server
|
* Protocol version 1, which shifts more responsibility onto the server
|
||||||
* This is a **major breaking** change!
|
* This is a **major breaking** change!
|
||||||
* The server now has a content of 'filter plugins', which transform data at various stages in the pipeline
|
* The server now has a content of 'filter plugins', which transform data at various stages in the pipeline
|
||||||
* The server now exposes Roblox instance objects instead of file contents, which lines up with how `rojo pack` will work, and paves the way for more robust syncing.
|
* The server now exposes Roblox instance objects instead of file contents, which lines up with how `rojo pack` will work, and paves the way for more robust syncing.
|
||||||
* Added `*.model.json` files, which let you embed small Roblox objects into your Rojo tree.
|
* Added `*.model.json` files, which let you embed small Roblox objects into your Rojo tree.
|
||||||
* Improved error messages in some cases ([#46](https://github.com/LPGhatguy/rojo/issues/46))
|
* Improved error messages in some cases ([#46](https://github.com/rojo-rbx/rojo/issues/46))
|
||||||
|
|
||||||
## [0.3.2](https://github.com/LPGhatguy/rojo/releases/tag/v0.3.2) (December 20, 2017)
|
## [0.3.2](https://github.com/rojo-rbx/rojo/releases/tag/v0.3.2) (December 20, 2017)
|
||||||
* Fixed `rojo serve` failing to correctly construct an absolute root path when passed as an argument
|
* Fixed `rojo serve` failing to correctly construct an absolute root path when passed as an argument
|
||||||
* Fixed intense CPU usage when running `rojo serve`
|
* Fixed intense CPU usage when running `rojo serve`
|
||||||
|
|
||||||
## [0.3.1](https://github.com/LPGhatguy/rojo/releases/tag/v0.3.1) (December 14, 2017)
|
## [0.3.1](https://github.com/rojo-rbx/rojo/releases/tag/v0.3.1) (December 14, 2017)
|
||||||
* Improved error reporting when invalid JSON is found in a `rojo.json` project
|
* Improved error reporting when invalid JSON is found in a `rojo.json` project
|
||||||
* These messages are passed on from Serde
|
* These messages are passed on from Serde
|
||||||
|
|
||||||
## [0.3.0](https://github.com/LPGhatguy/rojo/releases/tag/v0.3.0) (December 12, 2017)
|
## [0.3.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.3.0) (December 12, 2017)
|
||||||
* Factored out the plugin into a separate repository
|
* Factored out the plugin into a separate repository
|
||||||
* Fixed server when using a file as a partition
|
* Fixed server when using a file as a partition
|
||||||
* Previously, trailing slashes were put on the end of a partition even if the read request was an empty string. This broke file reading on Windows when a partition pointed to a file instead of a directory!
|
* Previously, trailing slashes were put on the end of a partition even if the read request was an empty string. This broke file reading on Windows when a partition pointed to a file instead of a directory!
|
||||||
* Started running automatic tests on Travis CI (#9)
|
* Started running automatic tests on Travis CI (#9)
|
||||||
|
|
||||||
## [0.2.3](https://github.com/LPGhatguy/rojo/releases/tag/v0.2.3) (December 4, 2017)
|
## [0.2.3](https://github.com/rojo-rbx/rojo/releases/tag/v0.2.3) (December 4, 2017)
|
||||||
* Plugin only release
|
* Plugin only release
|
||||||
* Tightened `init` file rules to only match script files
|
* Tightened `init` file rules to only match script files
|
||||||
* Previously, Rojo would sometimes pick up the wrong file when syncing
|
* Previously, Rojo would sometimes pick up the wrong file when syncing
|
||||||
|
|
||||||
## [0.2.2](https://github.com/LPGhatguy/rojo/releases/tag/v0.2.2) (December 1, 2017)
|
## [0.2.2](https://github.com/rojo-rbx/rojo/releases/tag/v0.2.2) (December 1, 2017)
|
||||||
* Plugin only release
|
* Plugin only release
|
||||||
* Fixed broken reconciliation behavior with `init` files
|
* Fixed broken reconciliation behavior with `init` files
|
||||||
|
|
||||||
## [0.2.1](https://github.com/LPGhatguy/rojo/releases/tag/v0.2.1) (December 1, 2017)
|
## [0.2.1](https://github.com/rojo-rbx/rojo/releases/tag/v0.2.1) (December 1, 2017)
|
||||||
* Plugin only release
|
* Plugin only release
|
||||||
* Changes default port to 8000
|
* Changes default port to 8000
|
||||||
|
|
||||||
## [0.2.0](https://github.com/LPGhatguy/rojo/releases/tag/v0.2.0) (December 1, 2017)
|
## [0.2.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.2.0) (December 1, 2017)
|
||||||
* Support for `init.lua` like rbxfs and rbxpacker
|
* Support for `init.lua` like rbxfs and rbxpacker
|
||||||
* More robust syncing with a new reconciler
|
* More robust syncing with a new reconciler
|
||||||
|
|
||||||
## [0.1.0](https://github.com/LPGhatguy/rojo/releases/tag/v0.1.0) (November 29, 2017)
|
## [0.1.0](https://github.com/rojo-rbx/rojo/releases/tag/v0.1.0) (November 29, 2017)
|
||||||
* Initial release, functionally very similar to [rbxfs](https://github.com/LPGhatguy/rbxfs)
|
* Initial release, functionally very similar to [rbxfs](https://github.com/rojo-rbx/rbxfs)
|
||||||
55
CONTRIBUTING.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Contributing to the Rojo Project
|
||||||
|
Rojo is a big project and can always use more help! This guide covers all repositories underneath the [rojo-rbx organization on GitHub](https://github.com/rojo-rbx).
|
||||||
|
|
||||||
|
Some of the repositories covered are:
|
||||||
|
|
||||||
|
* https://github.com/rojo-rbx/rojo
|
||||||
|
* https://github.com/rojo-rbx/rbx-dom
|
||||||
|
* https://github.com/rojo-rbx/vscode-rojo
|
||||||
|
* https://github.com/rojo-rbx/rbxlx-to-rojo
|
||||||
|
|
||||||
|
## Code
|
||||||
|
Code contributions are welcome for features and bugs that have been reported in the project's bug tracker. We want to make sure that no one wastes their time, so be sure to talk with maintainers about what changes would be accepted before doing any work!
|
||||||
|
|
||||||
|
You'll want these tools to work on Rojo:
|
||||||
|
|
||||||
|
* Latest stable Rust compiler
|
||||||
|
* Latest stable [Rojo](https://github.com/rojo-rbx/rojo)
|
||||||
|
* Latest stable [Remodel](https://github.com/rojo-rbx/remodel)
|
||||||
|
* Latest stable [run-in-roblox](https://github.com/rojo-rbx/run-in-roblox)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
Documentation impacts way more people than the individual lines of code we write.
|
||||||
|
|
||||||
|
If you find any problems in documentation, including typos, bad grammar, misleading phrasing, or missing content, feel free to file issues and pull requests to fix them.
|
||||||
|
|
||||||
|
## Bug Reports and Feature Requests
|
||||||
|
Most of the tools around Rojo try to be clear when an issue is a bug. Even if they aren't, sometimes things don't work quite right.
|
||||||
|
|
||||||
|
Sometimes there's something that Rojo doesn't do that it probably should.
|
||||||
|
|
||||||
|
Please file issues and we'll try to help figure out what the best way forward is.
|
||||||
|
|
||||||
|
## Pushing a Rojo Release
|
||||||
|
The Rojo release process is pretty manual right now. If you need to do it, here's how:
|
||||||
|
|
||||||
|
1. Bump server version in [`server/Cargo.toml`](server/Cargo.toml)
|
||||||
|
2. Bump plugin version in [`plugin/src/Config.lua`](plugin/src/Config.lua)
|
||||||
|
3. Run `cargo test` to update `Cargo.lock` and double-check tests
|
||||||
|
4. Update [`CHANGELOG.md`](CHANGELOG.md)
|
||||||
|
5. Commit!
|
||||||
|
* `git add . && git commit -m "Release vX.Y.Z"`
|
||||||
|
6. Tag the commit with the version from `Cargo.toml` prepended with a v, like `v0.4.13`
|
||||||
|
7. Build Windows release build of CLI
|
||||||
|
* `cargo build --release`
|
||||||
|
7. Publish the CLI
|
||||||
|
* `cargo publish`
|
||||||
|
8. Build and upload the plugin
|
||||||
|
* `rojo build plugin -o Rojo.rbxm`
|
||||||
|
* Upload `Rojo.rbxm` to Roblox.com, keep it for later
|
||||||
|
9. Push commits and tags
|
||||||
|
* `git push && git push --tags`
|
||||||
|
10. Copy GitHub release content from previous release
|
||||||
|
* Update the leading text with a summary about the release
|
||||||
|
* Paste the changelog notes (as-is!) from [`CHANGELOG.md`](CHANGELOG.md)
|
||||||
|
* Write a small summary of each major feature
|
||||||
2559
Cargo.lock
generated
100
Cargo.toml
@@ -1,5 +1,99 @@
|
|||||||
|
[package]
|
||||||
|
name = "rojo"
|
||||||
|
version = "0.6.0-alpha.2"
|
||||||
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
|
description = "Enables professional-grade development tools for Roblox developers"
|
||||||
|
license = "MPL-2.0"
|
||||||
|
homepage = "https://rojo.space"
|
||||||
|
documentation = "https://rojo.space/docs"
|
||||||
|
repository = "https://github.com/rojo-rbx/rojo"
|
||||||
|
readme = "README.md"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
"/plugin/**",
|
||||||
|
"/test-projects/**",
|
||||||
|
]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
|
||||||
|
# Turn on support for specifying glob ignore path rules in the project format.
|
||||||
|
unstable_glob_ignore_paths = []
|
||||||
|
|
||||||
|
# Turn on the server half of Rojo's unstable two-way sync feature.
|
||||||
|
unstable_two_way_sync = []
|
||||||
|
|
||||||
|
# Enable this feature to live-reload assets from the web UI.
|
||||||
|
dev_live_assets = []
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"server",
|
"rojo-test",
|
||||||
"rojo-e2e",
|
"rojo-insta-ext",
|
||||||
]
|
"clibrojo",
|
||||||
|
"vfs",
|
||||||
|
]
|
||||||
|
|
||||||
|
default-members = [
|
||||||
|
".",
|
||||||
|
"rojo-test",
|
||||||
|
"rojo-insta-ext",
|
||||||
|
"vfs",
|
||||||
|
]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "librojo"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rojo"
|
||||||
|
path = "src/bin.rs"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "build"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
crossbeam-channel = "0.4.0"
|
||||||
|
csv = "1.1.1"
|
||||||
|
env_logger = "0.7.1"
|
||||||
|
futures = "0.1.29"
|
||||||
|
globset = "0.4.4"
|
||||||
|
humantime = "1.3.0"
|
||||||
|
hyper = "0.12.35"
|
||||||
|
jod-thread = "0.1.0"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
log = "0.4.8"
|
||||||
|
maplit = "1.0.1"
|
||||||
|
notify = "4.0.14"
|
||||||
|
rbx_binary = "0.5.0"
|
||||||
|
rbx_dom_weak = "1.10.1"
|
||||||
|
rbx_reflection = "3.3.408"
|
||||||
|
rbx_xml = "0.11.3"
|
||||||
|
regex = "1.3.1"
|
||||||
|
reqwest = "0.9.20"
|
||||||
|
ritz = "0.1.0"
|
||||||
|
rlua = "0.17.0"
|
||||||
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
snafu = "0.6.0"
|
||||||
|
structopt = "0.3.5"
|
||||||
|
termcolor = "1.0.5"
|
||||||
|
uuid = { version = "0.8.1", features = ["v4", "serde"] }
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
winreg = "0.6.2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rojo-insta-ext = { path = "rojo-insta-ext" }
|
||||||
|
|
||||||
|
criterion = "0.3"
|
||||||
|
insta = { version = "0.13.1", features = ["redactions"] }
|
||||||
|
lazy_static = "1.2"
|
||||||
|
paste = "0.1"
|
||||||
|
pretty_assertions = "0.6.1"
|
||||||
|
serde_yaml = "0.8.9"
|
||||||
|
tempfile = "3.0"
|
||||||
|
tokio = "0.1.22"
|
||||||
|
walkdir = "2.1"
|
||||||
|
|||||||
61
README.md
@@ -1,68 +1,55 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/rojo-logo.png" alt="Rojo" height="217" />
|
<a href="https://rojo.space">
|
||||||
|
<img src="assets/logo-512.png" alt="Rojo" height="217" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div> </div>
|
<div> </div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://travis-ci.org/LPGhatguy/rojo">
|
<a href="https://github.com/rojo-rbx/rojo/actions">
|
||||||
<img src="https://api.travis-ci.org/LPGhatguy/rojo.svg?branch=master" alt="Travis-CI Build Status" />
|
<img src="https://github.com/rojo-rbx/rojo/workflows/CI/badge.svg" alt="Actions status" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://crates.io/crates/rojo">
|
<a href="https://crates.io/crates/rojo">
|
||||||
<img src="https://img.shields.io/crates/v/rojo.svg?label=version" alt="Latest server version" />
|
<img src="https://img.shields.io/crates/v/rojo.svg?label=latest%20release" alt="Latest server version" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://lpghatguy.github.io/rojo/0.4.x">
|
<a href="https://rojo.space/docs/0.5.x">
|
||||||
<img src="https://img.shields.io/badge/docs-0.4.x-brightgreen.svg" alt="Rojo Documentation" />
|
<img src="https://img.shields.io/badge/docs-0.5.x-brightgreen.svg" alt="Rojo 0.5.x Documentation" />
|
||||||
</a>
|
|
||||||
<a href="https://lpghatguy.github.io/rojo/0.5.x">
|
|
||||||
<img src="https://img.shields.io/badge/docs-0.5.x-brightgreen.svg" alt="Rojo Documentation" />
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
**Rojo** is a flexible multi-tool designed for creating robust Roblox projects.
|
**Rojo** is a tool designed to enable Roblox developers to use professional-grade software engineering tools.
|
||||||
|
|
||||||
It lets Roblox developers use industry-leading tools like Git and VS Code, and crucial utilities like Luacheck.
|
With Rojo, it's possible to use industry-leading tools like **Visual Studio Code** and **Git**.
|
||||||
|
|
||||||
Rojo is designed for **power users** who want to use the **best tools available** for building games, libraries, and plugins.
|
Rojo is designed for power users who want to use the best tools available for building games, libraries, and plugins.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
Rojo lets you:
|
Rojo enables:
|
||||||
|
|
||||||
* Work on scripts from the filesystem, in your favorite editor
|
* Working on scripts and models from the filesystem, in your favorite editor
|
||||||
* Version your place, model, or plugin using Git or another VCS
|
* Versioning your game, library, or plugin using Git or another VCS
|
||||||
* Sync `rbxmx` and `rbxm` models into your game in real time
|
* Streaming `rbxmx` and `rbxm` models into your game in real time
|
||||||
* Package and deploy your project to Roblox.com from the command line
|
* Packaging and deploying your project to Roblox.com from the command line
|
||||||
|
|
||||||
Soon, Rojo will be able to:
|
Soon, Rojo will be able to:
|
||||||
|
|
||||||
|
* Automatically convert your existing game to work with Rojo
|
||||||
* Sync instances from Roblox Studio to the filesystem
|
* Sync instances from Roblox Studio to the filesystem
|
||||||
* Compile MoonScript and other custom things for your project
|
* Automatically manage your assets on Roblox.com, like images and sounds
|
||||||
|
* Import custom instances like MoonScript code
|
||||||
|
|
||||||
## [Documentation](https://lpghatguy.github.io/rojo)
|
## [Documentation](https://rojo.space/docs)
|
||||||
You can also view the documentation by browsing the [docs](https://github.com/LPGhatguy/rojo/tree/master/docs) folder of the repository, but because it uses a number of Markdown extensions, it may not be very readable.
|
Documentation is hosted in the [rojo.space repository](https://github.com/rojo-rbx/rojo.space).
|
||||||
|
|
||||||
## Inspiration and Alternatives
|
|
||||||
There are lots of other tools that sync scripts into Roblox or provide other tools for working with Roblox places.
|
|
||||||
|
|
||||||
Here are a few, if you're looking for alternatives or supplements to Rojo:
|
|
||||||
|
|
||||||
* [rbxmk by Anaminus](https://github.com/anaminus/rbxmk)
|
|
||||||
* [Rofresh by Osyris](https://github.com/osyrisrblx/rofresh)
|
|
||||||
* [RbxRefresh by Osyris](https://github.com/osyrisrblx/RbxRefresh)
|
|
||||||
* [Studio Bridge by Vocksel](https://github.com/vocksel/studio-bridge)
|
|
||||||
* [Elixir by Vocksel](https://github.com/vocksel/elixir)
|
|
||||||
* [RbxSync by evaera](https://github.com/evaera/RbxSync)
|
|
||||||
* [CodeSync by MemoryPenguin](https://github.com/MemoryPenguin/CodeSync)
|
|
||||||
* [rbx-exteditor by MemoryPenguin](https://github.com/MemoryPenguin/rbx-exteditor)
|
|
||||||
|
|
||||||
If you use a plugin that _isn't_ Rojo for syncing code, open an issue and let me know why! I'd like Rojo to be the end-all tool so that people stop reinventing solutions to this problem.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
Check out our [contribution guide](CONTRIBUTING.md) for detailed instructions for helping work on Rojo!
|
||||||
|
|
||||||
Pull requests are welcome!
|
Pull requests are welcome!
|
||||||
|
|
||||||
Rojo supports Rust 1.31.1 and newer. Any changes to the minimum required compiler version require a _minor_ version bump.
|
Rojo supports Rust 1.40.0 and newer. The minimum supported version of Rust is based on the latest versions of the dependencies that Rojo has.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
Rojo is available under the terms of the Mozilla Public License, Version 2.0. See [LICENSE.txt](LICENSE.txt) for details.
|
||||||
BIN
assets/icon-32.png
Normal file
|
After Width: | Height: | Size: 975 B |
181
assets/index.css
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
text-decoration: inherit;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
box-sizing: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width:100%;
|
||||||
|
max-height:100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-list > li {
|
||||||
|
margin-left: 1.2em;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 50rem;
|
||||||
|
background-color: #efefef;
|
||||||
|
border: 1px solid #666;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex: 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-logo {
|
||||||
|
flex: 0 0 10rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
flex: 0 0 20rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-name {
|
||||||
|
display: inline;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-section:not(:last-of-type) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-list {
|
||||||
|
flex: 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin: -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid #666;
|
||||||
|
padding: 0.3em 1em;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-section {
|
||||||
|
margin: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-items {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-label > label {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-input ~ .expandable-label .expandable-visualizer {
|
||||||
|
font-family: monospace;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
text-align: center;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
transition: transform 100ms ease-in-out;
|
||||||
|
transform-origin: 60% 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-visualizer::before {
|
||||||
|
content: "›";
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-input:checked ~ .expandable-label {
|
||||||
|
border-bottom: 1px solid #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-input:checked ~ .expandable-label .expandable-visualizer {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandable-input:not(:checked) ~ .expandable-items {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vfs-entry {
|
||||||
|
}
|
||||||
|
|
||||||
|
.vfs-entry-name {
|
||||||
|
position: relative;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vfs-entry-children .vfs-entry-name::before {
|
||||||
|
content: "";
|
||||||
|
width: 0.6em;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #999;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: -0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vfs-entry-note {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vfs-entry-children {
|
||||||
|
padding-left: 0.8em;
|
||||||
|
margin-left: 0.2em;
|
||||||
|
border-left: 1px solid #999;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 13 KiB |
@@ -1,41 +0,0 @@
|
|||||||
<TextureAtlas imagePath="sheet.png">
|
|
||||||
<SubTexture name="grey_arrowDownGrey.png" x="78" y="498" width="15" height="10"/>
|
|
||||||
<SubTexture name="grey_arrowDownWhite.png" x="123" y="496" width="15" height="10"/>
|
|
||||||
<SubTexture name="grey_arrowUpGrey.png" x="108" y="498" width="15" height="10"/>
|
|
||||||
<SubTexture name="grey_arrowUpWhite.png" x="93" y="498" width="15" height="10"/>
|
|
||||||
<SubTexture name="grey_box.png" x="147" y="433" width="38" height="36"/>
|
|
||||||
<SubTexture name="grey_boxCheckmark.png" x="147" y="469" width="38" height="36"/>
|
|
||||||
<SubTexture name="grey_boxCross.png" x="185" y="433" width="38" height="36"/>
|
|
||||||
<SubTexture name="grey_boxTick.png" x="190" y="198" width="36" height="36"/>
|
|
||||||
<SubTexture name="grey_button00.png" x="0" y="143" width="190" height="45"/>
|
|
||||||
<SubTexture name="grey_button01.png" x="0" y="188" width="190" height="49"/>
|
|
||||||
<SubTexture name="grey_button02.png" x="0" y="98" width="190" height="45"/>
|
|
||||||
<SubTexture name="grey_button03.png" x="0" y="331" width="190" height="49"/>
|
|
||||||
<SubTexture name="grey_button04.png" x="0" y="286" width="190" height="45"/>
|
|
||||||
<SubTexture name="grey_button05.png" x="0" y="0" width="195" height="49"/>
|
|
||||||
<SubTexture name="grey_button06.png" x="0" y="49" width="191" height="49"/>
|
|
||||||
<SubTexture name="grey_button07.png" x="195" y="0" width="49" height="49"/>
|
|
||||||
<SubTexture name="grey_button08.png" x="240" y="49" width="49" height="49"/>
|
|
||||||
<SubTexture name="grey_button09.png" x="98" y="433" width="49" height="45"/>
|
|
||||||
<SubTexture name="grey_button10.png" x="191" y="49" width="49" height="49"/>
|
|
||||||
<SubTexture name="grey_button11.png" x="0" y="433" width="49" height="45"/>
|
|
||||||
<SubTexture name="grey_button12.png" x="244" y="0" width="49" height="49"/>
|
|
||||||
<SubTexture name="grey_button13.png" x="49" y="433" width="49" height="45"/>
|
|
||||||
<SubTexture name="grey_button14.png" x="0" y="384" width="190" height="49"/>
|
|
||||||
<SubTexture name="grey_button15.png" x="0" y="237" width="190" height="49"/>
|
|
||||||
<SubTexture name="grey_checkmarkGrey.png" x="99" y="478" width="21" height="20"/>
|
|
||||||
<SubTexture name="grey_checkmarkWhite.png" x="78" y="478" width="21" height="20"/>
|
|
||||||
<SubTexture name="grey_circle.png" x="185" y="469" width="36" height="36"/>
|
|
||||||
<SubTexture name="grey_crossGrey.png" x="120" y="478" width="18" height="18"/>
|
|
||||||
<SubTexture name="grey_crossWhite.png" x="190" y="318" width="18" height="18"/>
|
|
||||||
<SubTexture name="grey_panel.png" x="190" y="98" width="100" height="100"/>
|
|
||||||
<SubTexture name="grey_sliderDown.png" x="190" y="234" width="28" height="42"/>
|
|
||||||
<SubTexture name="grey_sliderEnd.png" x="138" y="478" width="8" height="10"/>
|
|
||||||
<SubTexture name="grey_sliderHorizontal.png" x="0" y="380" width="190" height="4"/>
|
|
||||||
<SubTexture name="grey_sliderLeft.png" x="0" y="478" width="39" height="31"/>
|
|
||||||
<SubTexture name="grey_sliderRight.png" x="39" y="478" width="39" height="31"/>
|
|
||||||
<SubTexture name="grey_sliderUp.png" x="190" y="276" width="28" height="42"/>
|
|
||||||
<SubTexture name="grey_sliderVertical.png" x="208" y="318" width="4" height="100"/>
|
|
||||||
<SubTexture name="grey_tickGrey.png" x="190" y="336" width="17" height="17"/>
|
|
||||||
<SubTexture name="grey_tickWhite.png" x="190" y="353" width="17" height="17"/>
|
|
||||||
</TextureAtlas>
|
|
||||||
BIN
assets/logo-512.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
66
assets/place.project.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"name": "[placeholder]",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"HttpService": {
|
||||||
|
"$className": "HttpService",
|
||||||
|
"$properties": {
|
||||||
|
"HttpEnabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Lighting": {
|
||||||
|
"$className": "Lighting",
|
||||||
|
"$properties": {
|
||||||
|
"Ambient": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"Brightness": 2,
|
||||||
|
"GlobalShadows": true,
|
||||||
|
"Outlines": false,
|
||||||
|
"Technology": "Voxel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"$className": "ReplicatedStorage",
|
||||||
|
"Source": {
|
||||||
|
"$path": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SoundService": {
|
||||||
|
"$className": "SoundService",
|
||||||
|
"$properties": {
|
||||||
|
"RespectFilteringEnabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Workspace": {
|
||||||
|
"$className": "Workspace",
|
||||||
|
"$properties": {
|
||||||
|
"FilteringEnabled": true
|
||||||
|
},
|
||||||
|
"Baseplate": {
|
||||||
|
"$className": "Part",
|
||||||
|
"$properties": {
|
||||||
|
"Anchored": true,
|
||||||
|
"Color": [
|
||||||
|
0.38823,
|
||||||
|
0.37254,
|
||||||
|
0.38823
|
||||||
|
],
|
||||||
|
"Locked": true,
|
||||||
|
"Position": [
|
||||||
|
0,
|
||||||
|
-10,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"Size": [
|
||||||
|
512,
|
||||||
|
20,
|
||||||
|
512
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 375 B |
|
Before Width: | Height: | Size: 382 B |
|
Before Width: | Height: | Size: 430 B |
BIN
assets/test-folder.rbxm
Normal file
42
benches/build.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||||
|
use tempfile::{tempdir, TempDir};
|
||||||
|
|
||||||
|
use librojo::cli::{build, BuildCommand};
|
||||||
|
|
||||||
|
pub fn benchmark_small_place(c: &mut Criterion) {
|
||||||
|
bench_build_place(c, "Small Place", "test-projects/benchmark_small_place")
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, benchmark_small_place);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
||||||
|
fn bench_build_place(c: &mut Criterion, name: &str, path: &str) {
|
||||||
|
let mut group = c.benchmark_group(name);
|
||||||
|
|
||||||
|
// 'rojo build' generally takes a fair bit of time to execute.
|
||||||
|
group.sample_size(10);
|
||||||
|
group.bench_function("build", |b| {
|
||||||
|
b.iter_batched(
|
||||||
|
|| place_setup(path),
|
||||||
|
|(_dir, options)| build(options).unwrap(),
|
||||||
|
BatchSize::SmallInput,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn place_setup<P: AsRef<Path>>(input_path: P) -> (TempDir, BuildCommand) {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let input = input_path.as_ref().to_path_buf();
|
||||||
|
let output = dir.path().join("output.rbxlx");
|
||||||
|
|
||||||
|
let options = BuildCommand {
|
||||||
|
project: input,
|
||||||
|
output,
|
||||||
|
};
|
||||||
|
|
||||||
|
(dir, options)
|
||||||
|
}
|
||||||
5
bin/dev-plugin.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
watchexec -c -w plugin "sh -c './bin/install-dev-plugin.sh'"
|
||||||
13
bin/install-dev-plugin.sh
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$( mktemp -d )"
|
||||||
|
PLUGIN_FILE="$DIR/Rojo.rbxm"
|
||||||
|
TESTEZ_FILE="$DIR/TestEZ.rbxm"
|
||||||
|
|
||||||
|
rojo build plugin -o "$PLUGIN_FILE"
|
||||||
|
rojo build plugin/testez.project.json -o "$TESTEZ_FILE"
|
||||||
|
remodel bin/mark-plugin-as-dev.lua "$PLUGIN_FILE" "$TESTEZ_FILE" 2>/dev/null
|
||||||
|
|
||||||
|
cp "$PLUGIN_FILE" "$LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm"
|
||||||
5
bin/install-release-plugin.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
rojo build plugin -o "$LOCALAPPDATA/Roblox/Plugins/Rojo.rbxm"
|
||||||
12
bin/mark-plugin-as-dev.lua
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
local pluginPath, testezPath = ...
|
||||||
|
|
||||||
|
local plugin = remodel.readModelFile(pluginPath)[1]
|
||||||
|
local testez = remodel.readModelFile(testezPath)[1]
|
||||||
|
|
||||||
|
local marker = Instance.new("Folder")
|
||||||
|
marker.Name = "ROJO_DEV_BUILD"
|
||||||
|
marker.Parent = plugin
|
||||||
|
|
||||||
|
testez.Parent = plugin
|
||||||
|
|
||||||
|
remodel.writeModelFile(plugin, pluginPath)
|
||||||
8
bin/put-plugin-in-test-place.lua
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
local pluginPath, placePath = ...
|
||||||
|
|
||||||
|
local plugin = remodel.readModelFile(pluginPath)[1]
|
||||||
|
local place = remodel.readPlaceFile(placePath)
|
||||||
|
|
||||||
|
plugin.Parent = place:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
|
remodel.writePlaceFile(place, placePath)
|
||||||
6
bin/run-all-tests.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
./bin/run-cli-tests.sh
|
||||||
|
./bin/run-plugin-tests.sh
|
||||||
9
bin/run-cli-tests.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cargo test --all --locked
|
||||||
|
cargo fmt -- --check
|
||||||
|
|
||||||
|
touch src/lib.rs # Nudge Rust source to make Clippy actually check things
|
||||||
|
cargo clippy
|
||||||
16
bin/run-plugin-tests.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="$( mktemp -d )"
|
||||||
|
PLUGIN_FILE="$DIR/Rojo.rbxmx"
|
||||||
|
PLACE_FILE="$DIR/RojoTestPlace.rbxlx"
|
||||||
|
|
||||||
|
rojo build plugin -o "$PLUGIN_FILE"
|
||||||
|
rojo build plugin/place.project.json -o "$PLACE_FILE"
|
||||||
|
|
||||||
|
remodel bin/put-plugin-in-test-place.lua "$PLUGIN_FILE" "$PLACE_FILE"
|
||||||
|
|
||||||
|
run-in-roblox -s plugin/testBootstrap.server.lua "$PLACE_FILE"
|
||||||
|
|
||||||
|
luacheck plugin/src plugin/log plugin/http
|
||||||
13
clibrojo/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "clibrojo"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Lucien Greathouse <me@lpghatguy.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rojo = { path = ".." }
|
||||||
19
clibrojo/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Rojo as a C Library
|
||||||
|
This is an experiment to expose a C API for Rojo that would be suitable for embedding it into an existing C/C++ application.
|
||||||
|
|
||||||
|
I'm hoping to expand it to drop the HTTP layer and communicate through a channel, which could make it feasible to embed into an existing Roblox IDE with minimal changes or additional code.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
This project is currently not built by default and could break/disappear at any time.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build -p clibrojo
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows, Cargo will generate a `clibrojo.dll` and associated `.lib` file. Link these into your project.
|
||||||
|
|
||||||
|
To generate the associated C header file to include in the project, use [cbindgen](https://github.com/eqrion/cbindgen):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cbindgen --crate clibrojo --output include/rojo.h
|
||||||
|
```
|
||||||
14
clibrojo/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use std::{ffi::CStr, os::raw::c_char, path::PathBuf};
|
||||||
|
|
||||||
|
use librojo::commands::{serve, ServeOptions};
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn rojo_serve(path: *const c_char) {
|
||||||
|
let path = unsafe { PathBuf::from(CStr::from_ptr(path).to_str().unwrap()) };
|
||||||
|
|
||||||
|
serve(&ServeOptions {
|
||||||
|
fuzzy_project_path: path,
|
||||||
|
port: None,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
63
design.gv
@@ -1,39 +1,30 @@
|
|||||||
digraph G {
|
digraph Rojo {
|
||||||
graph [
|
concentrate = true;
|
||||||
ranksep = "0.7",
|
node [fontname = "sans-serif"];
|
||||||
nodesep = "1.0",
|
|
||||||
];
|
|
||||||
node [
|
|
||||||
fontname = "Hack",
|
|
||||||
shape = "record",
|
|
||||||
];
|
|
||||||
|
|
||||||
roblox_studio -> plugin [dir = "both"];
|
plugin [label="Roblox Studio Plugin"]
|
||||||
plugin -> web_server [style = "dashed", dir = "both"];
|
session [label="Session"]
|
||||||
|
rbx_tree [label="Instance Tree"]
|
||||||
|
imfs [label="In-Memory Filesystem"]
|
||||||
|
fs_impl [label="Filesystem Implementation\n(stubbed in tests)"]
|
||||||
|
fs [label="Real Filesystem"]
|
||||||
|
snapshot_subsystem [label="Snapshot Subsystem\n(reconciler)"]
|
||||||
|
snapshot_generator [label="Snapshot Generator"]
|
||||||
|
user_middleware [label="User Middleware\n(MoonScript, etc.)"]
|
||||||
|
builtin_middleware [label="Built-in Middleware\n(.lua, .rbxm, etc.)"]
|
||||||
|
api [label="Web API"]
|
||||||
|
file_watcher [label="File Watcher"]
|
||||||
|
|
||||||
web_server -> session;
|
session -> imfs
|
||||||
|
session -> rbx_tree
|
||||||
session -> rbx_session;
|
session -> snapshot_subsystem
|
||||||
session -> fs_watcher;
|
session -> snapshot_generator
|
||||||
session -> message_queue;
|
session -> file_watcher [dir="both"]
|
||||||
|
file_watcher -> imfs
|
||||||
fs_watcher -> imfs [weight = "10"];
|
snapshot_generator -> user_middleware
|
||||||
fs_watcher -> rbx_session [constraint = "false"];
|
snapshot_generator -> builtin_middleware
|
||||||
|
plugin -> api [style="dotted"; dir="both"; minlen=2]
|
||||||
imfs -> fs;
|
api -> session
|
||||||
|
imfs -> fs_impl
|
||||||
rbx_session -> imfs;
|
fs_impl -> fs
|
||||||
rbx_session -> middlewares [weight = "10"];
|
|
||||||
rbx_session -> message_queue [constraint = "false"];
|
|
||||||
|
|
||||||
plugin [label = "Studio Plugin"];
|
|
||||||
roblox_studio [label = "Roblox Studio"];
|
|
||||||
fs [label = "Filesystem"];
|
|
||||||
fs_watcher [label = "Filesystem Watcher"];
|
|
||||||
session [label = "Session"];
|
|
||||||
web_server [label = "Web API"];
|
|
||||||
imfs [label = "In-Memory Filesystem"];
|
|
||||||
rbx_session [label = "RbxSession"];
|
|
||||||
message_queue [label = "MessageQueue"];
|
|
||||||
middlewares [label = "Middlewares"];
|
|
||||||
}
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
[TOC]
|
|
||||||
|
|
||||||
## Creating the Rojo Project
|
|
||||||
|
|
||||||
To use Rojo to build a place, you'll need to create a new project file, which tells Rojo how your project is structured on-disk and in Roblox.
|
|
||||||
|
|
||||||
Create a new folder, then run `rojo init` inside that folder to initialize an empty project.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
mkdir my-new-project
|
|
||||||
cd my-new-project
|
|
||||||
|
|
||||||
rojo init
|
|
||||||
```
|
|
||||||
|
|
||||||
Rojo will make a small project file in your directory, named `default.project.json`. It'll make sure that any code in the directory `src` will get put into `ReplicatedStorage.Source`.
|
|
||||||
|
|
||||||
Speaking of, let's make sure we create a directory named `src`, and maybe a Lua file inside of it:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
mkdir src
|
|
||||||
echo 'print("Hello, world!")' > src/hello.lua
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building Your Place
|
|
||||||
Now that we have a project, one thing we can do is build a Roblox place file for our project. This is a great way to get started with a project quickly with no fuss.
|
|
||||||
|
|
||||||
All we have to do is call `rojo build`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
rojo build -o MyNewProject.rbxl
|
|
||||||
```
|
|
||||||
|
|
||||||
If you open `MyNewProject.rbxl` in Roblox Studio now, you should see a `Folder` containing a `ModuleScript` under `ReplicatedStorage`!
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
To generate an XML place file instead, like if you're checking the place file into version control, just use `rbxlx` as the extension on the output file instead.
|
|
||||||
|
|
||||||
## Live-Syncing into Studio
|
|
||||||
Building a place file is great for the initial build, but for actively working on your place, you'll want something quicker.
|
|
||||||
|
|
||||||
In Roblox Studio, make sure the Rojo plugin is installed. If you need it, check out [the installation guide](installation) to learn how to install it.
|
|
||||||
|
|
||||||
To expose your project to the plugin, you'll need to _serve_ it from the command line:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
rojo serve
|
|
||||||
```
|
|
||||||
|
|
||||||
This will start up a web server that tells Roblox Studio what instances are in your project and sends notifications if any of them change.
|
|
||||||
|
|
||||||
Note the port number, then switch into Roblox Studio and press the Rojo **Connect** button in the plugins tab. Type in the port number, if necessary, and press **Start**.
|
|
||||||
|
|
||||||
If everything went well, you should now be able to change files in the `src` directory and watch them sync into Roblox Studio in real time!
|
|
||||||
|
|
||||||
## Uploading Your Place
|
|
||||||
Aimed at teams that want serious levels of automation, Rojo can upload places to Roblox.com automatically.
|
|
||||||
|
|
||||||
You'll need an existing place on Roblox.com as well as the `.ROBLOSECURITY` cookie of an account that has write access to that place.
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
It's recommended that you set up a Roblox account dedicated to deploying your place instead of your personal account in case your security cookie is compromised.
|
|
||||||
|
|
||||||
Generating and uploading your place file is as simple as:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
rojo upload --asset_id [PLACE ID] --cookie "[SECURITY COOKIE]"
|
|
||||||
```
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.md-typeset__table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 19 KiB |
@@ -1,17 +0,0 @@
|
|||||||
digraph "Sync Files" {
|
|
||||||
graph [
|
|
||||||
ranksep = "0.7",
|
|
||||||
nodesep = "0.5",
|
|
||||||
];
|
|
||||||
node [
|
|
||||||
fontname = "monospace",
|
|
||||||
shape = "record",
|
|
||||||
];
|
|
||||||
|
|
||||||
my_model [label = "MyModel"]
|
|
||||||
init_server [label = "init.server.lua"]
|
|
||||||
foo [label = "foo.lua"]
|
|
||||||
|
|
||||||
my_model -> init_server
|
|
||||||
my_model -> foo
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
|
||||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
|
|
||||||
-->
|
|
||||||
<!-- Title: Sync Files Pages: 1 -->
|
|
||||||
<svg width="258pt" height="132pt"
|
|
||||||
viewBox="0.00 0.00 258.00 132.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 128)">
|
|
||||||
<title>Sync Files</title>
|
|
||||||
<polygon fill="white" stroke="none" points="-4,4 -4,-128 254,-128 254,4 -4,4"/>
|
|
||||||
<!-- my_model -->
|
|
||||||
<g id="node1" class="node"><title>my_model</title>
|
|
||||||
<polygon fill="none" stroke="black" points="104,-87.5 104,-123.5 178,-123.5 178,-87.5 104,-87.5"/>
|
|
||||||
<text text-anchor="middle" x="141" y="-101.8" font-family="monospace" font-size="14.00">MyModel</text>
|
|
||||||
</g>
|
|
||||||
<!-- init_server -->
|
|
||||||
<g id="node2" class="node"><title>init_server</title>
|
|
||||||
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 140,-36.5 140,-0.5 0,-0.5"/>
|
|
||||||
<text text-anchor="middle" x="70" y="-14.8" font-family="monospace" font-size="14.00">init.server.lua</text>
|
|
||||||
</g>
|
|
||||||
<!-- my_model->init_server -->
|
|
||||||
<g id="edge1" class="edge"><title>my_model->init_server</title>
|
|
||||||
<path fill="none" stroke="black" d="M126.632,-87.299C116.335,-74.9713 102.308,-58.1787 90.7907,-44.3902"/>
|
|
||||||
<polygon fill="black" stroke="black" points="93.4435,-42.1065 84.3465,-36.6754 88.0711,-46.594 93.4435,-42.1065"/>
|
|
||||||
</g>
|
|
||||||
<!-- foo -->
|
|
||||||
<g id="node3" class="node"><title>foo</title>
|
|
||||||
<polygon fill="none" stroke="black" points="176,-0.5 176,-36.5 250,-36.5 250,-0.5 176,-0.5"/>
|
|
||||||
<text text-anchor="middle" x="213" y="-14.8" font-family="monospace" font-size="14.00">foo.lua</text>
|
|
||||||
</g>
|
|
||||||
<!-- my_model->foo -->
|
|
||||||
<g id="edge2" class="edge"><title>my_model->foo</title>
|
|
||||||
<path fill="none" stroke="black" d="M155.57,-87.299C166.013,-74.9713 180.237,-58.1787 191.917,-44.3902"/>
|
|
||||||
<polygon fill="black" stroke="black" points="194.659,-46.5681 198.451,-36.6754 189.317,-42.0437 194.659,-46.5681"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,15 +0,0 @@
|
|||||||
digraph "Sync Files" {
|
|
||||||
graph [
|
|
||||||
ranksep = "0.7",
|
|
||||||
nodesep = "0.5",
|
|
||||||
];
|
|
||||||
node [
|
|
||||||
fontname = "monospace",
|
|
||||||
shape = "record",
|
|
||||||
];
|
|
||||||
|
|
||||||
my_model [label = "MyModel (Script)"]
|
|
||||||
foo [label = "foo (ModuleScript)"]
|
|
||||||
|
|
||||||
my_model -> foo
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
|
||||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
|
|
||||||
-->
|
|
||||||
<!-- Title: Sync Files Pages: 1 -->
|
|
||||||
<svg width="173pt" height="132pt"
|
|
||||||
viewBox="0.00 0.00 173.00 132.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 128)">
|
|
||||||
<title>Sync Files</title>
|
|
||||||
<polygon fill="white" stroke="none" points="-4,4 -4,-128 169,-128 169,4 -4,4"/>
|
|
||||||
<!-- my_model -->
|
|
||||||
<g id="node1" class="node"><title>my_model</title>
|
|
||||||
<polygon fill="none" stroke="black" points="8,-87.5 8,-123.5 157,-123.5 157,-87.5 8,-87.5"/>
|
|
||||||
<text text-anchor="middle" x="82.5" y="-101.8" font-family="monospace" font-size="14.00">MyModel (Script)</text>
|
|
||||||
</g>
|
|
||||||
<!-- foo -->
|
|
||||||
<g id="node2" class="node"><title>foo</title>
|
|
||||||
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 165,-36.5 165,-0.5 0,-0.5"/>
|
|
||||||
<text text-anchor="middle" x="82.5" y="-14.8" font-family="monospace" font-size="14.00">foo (ModuleScript)</text>
|
|
||||||
</g>
|
|
||||||
<!-- my_model->foo -->
|
|
||||||
<g id="edge1" class="edge"><title>my_model->foo</title>
|
|
||||||
<path fill="none" stroke="black" d="M82.5,-87.299C82.5,-75.6626 82.5,-60.0479 82.5,-46.7368"/>
|
|
||||||
<polygon fill="black" stroke="black" points="86.0001,-46.6754 82.5,-36.6754 79.0001,-46.6755 86.0001,-46.6754"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,17 +0,0 @@
|
|||||||
digraph "Sync Files" {
|
|
||||||
graph [
|
|
||||||
ranksep = "0.7",
|
|
||||||
nodesep = "0.5",
|
|
||||||
];
|
|
||||||
node [
|
|
||||||
fontname = "monospace",
|
|
||||||
shape = "record",
|
|
||||||
];
|
|
||||||
|
|
||||||
model [label = "My Cool Model (Folder)"]
|
|
||||||
root_part [label = "RootPart (Part)"]
|
|
||||||
send_money [label = "SendMoney (RemoteEvent)"]
|
|
||||||
|
|
||||||
model -> root_part
|
|
||||||
model -> send_money
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
|
||||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<!-- Generated by graphviz version 2.38.0 (20140413.2041)
|
|
||||||
-->
|
|
||||||
<!-- Title: Sync Files Pages: 1 -->
|
|
||||||
<svg width="390pt" height="132pt"
|
|
||||||
viewBox="0.00 0.00 390.00 132.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 128)">
|
|
||||||
<title>Sync Files</title>
|
|
||||||
<polygon fill="white" stroke="none" points="-4,4 -4,-128 386,-128 386,4 -4,4"/>
|
|
||||||
<!-- model -->
|
|
||||||
<g id="node1" class="node"><title>model</title>
|
|
||||||
<polygon fill="none" stroke="black" points="75,-87.5 75,-123.5 273,-123.5 273,-87.5 75,-87.5"/>
|
|
||||||
<text text-anchor="middle" x="174" y="-101.8" font-family="monospace" font-size="14.00">My Cool Model (Folder)</text>
|
|
||||||
</g>
|
|
||||||
<!-- root_part -->
|
|
||||||
<g id="node2" class="node"><title>root_part</title>
|
|
||||||
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 140,-36.5 140,-0.5 0,-0.5"/>
|
|
||||||
<text text-anchor="middle" x="70" y="-14.8" font-family="monospace" font-size="14.00">RootPart (Part)</text>
|
|
||||||
</g>
|
|
||||||
<!-- model->root_part -->
|
|
||||||
<g id="edge1" class="edge"><title>model->root_part</title>
|
|
||||||
<path fill="none" stroke="black" d="M152.954,-87.299C137.448,-74.6257 116.168,-57.2335 99.0438,-43.2377"/>
|
|
||||||
<polygon fill="black" stroke="black" points="100.972,-40.2938 91.0147,-36.6754 96.5426,-45.7138 100.972,-40.2938"/>
|
|
||||||
</g>
|
|
||||||
<!-- send_money -->
|
|
||||||
<g id="node3" class="node"><title>send_money</title>
|
|
||||||
<polygon fill="none" stroke="black" points="176,-0.5 176,-36.5 382,-36.5 382,-0.5 176,-0.5"/>
|
|
||||||
<text text-anchor="middle" x="279" y="-14.8" font-family="monospace" font-size="14.00">SendMoney (RemoteEvent)</text>
|
|
||||||
</g>
|
|
||||||
<!-- model->send_money -->
|
|
||||||
<g id="edge2" class="edge"><title>model->send_money</title>
|
|
||||||
<path fill="none" stroke="black" d="M195.248,-87.299C210.904,-74.6257 232.388,-57.2335 249.677,-43.2377"/>
|
|
||||||
<polygon fill="black" stroke="black" points="252.213,-45.6878 257.783,-36.6754 247.809,-40.2471 252.213,-45.6878"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,11 +0,0 @@
|
|||||||
This is the documentation home for Rojo 0.5.x.
|
|
||||||
|
|
||||||
Available versions of these docs:
|
|
||||||
|
|
||||||
* [Latest version (currently 0.5.x)](https://lpghatguy.github.io/rojo)
|
|
||||||
* [0.5.x](https://lpghatguy.github.io/rojo/0.5.x)
|
|
||||||
* [0.4.x](https://lpghatguy.github.io/rojo/0.4.x)
|
|
||||||
|
|
||||||
**Rojo** is a flexible multi-tool designed for creating robust Roblox projects.
|
|
||||||
|
|
||||||
This documentation is a continual work in progress. If you find any issues, please file an issue on [Rojo's issue tracker](https://github.com/LPGhatguy/rojo/issues)!
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
[TOC]
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Rojo has two components:
|
|
||||||
|
|
||||||
* The command line interface (CLI)
|
|
||||||
* The Roblox Studio plugin
|
|
||||||
|
|
||||||
!!! info
|
|
||||||
It's important that your installed version of the plugin and CLI are compatible.
|
|
||||||
|
|
||||||
The plugin will show errors in the Roblox Studio output window if there is a version mismatch.
|
|
||||||
|
|
||||||
## Installing the CLI
|
|
||||||
|
|
||||||
### Installing from GitHub
|
|
||||||
If you're on Windows, there are pre-built binaries available from Rojo's [GitHub Releases page](https://github.com/LPGhatguy/rojo/releases).
|
|
||||||
|
|
||||||
The Rojo CLI must be run from the command line, like Terminal.app on MacOS or `cmd.exe` on Windows. It's recommended that you put the Rojo CLI executable on your `PATH` to make this easier.
|
|
||||||
|
|
||||||
### Installing from Cargo
|
|
||||||
If you have Rust installed, the easiest way to get Rojo is with Cargo!
|
|
||||||
|
|
||||||
To install the latest 0.5.0 alpha, use:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo install rojo --version 0.5.0-alpha.3
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installing the Plugin
|
|
||||||
|
|
||||||
### Installing from GitHub
|
|
||||||
The Rojo Roblox Studio plugin is available available from Rojo's [GitHub Releases page](https://github.com/LPGhatguy/rojo/releases).
|
|
||||||
|
|
||||||
Download the attached `rbxm` file and put it into your Roblox Studio plugins folder. You can find that folder by pressing **Plugins Folder** from your Plugins toolbar in Roblox Studio:
|
|
||||||
|
|
||||||

|
|
||||||
{: align="center" }
|
|
||||||
|
|
||||||
### Installing from Roblox.com
|
|
||||||
Visit [Rojo's Roblox.com Plugin page](https://www.roblox.com/library/1997686364/Rojo-0-5-0-alpha-3) in Roblox Studio and press **Install**.
|
|
||||||
|
|
||||||
## Visual Studio Code Extension
|
|
||||||
If you use Visual Studio Code on Windows, you can install [Evaera's unofficial Rojo extension](https://marketplace.visualstudio.com/items?itemName=evaera.vscode-rojo), which will install both halves of Rojo for you. It even has a nifty UI to add partitions and start/stop the Rojo server!
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
This document aims to give a general overview of how Rojo works. It's intended for people who want to contribute to the project as well as anyone who's just curious how the tool works!
|
|
||||||
|
|
||||||
[TOC]
|
|
||||||
|
|
||||||
## CLI
|
|
||||||
|
|
||||||
### RbxTree
|
|
||||||
Rojo uses a library named [`rbx_tree`](https://github.com/LPGhatguy/rbx-tree) as its implementation of the Roblox DOM. It serves as a common format for serialization to all the formats Rojo supports!
|
|
||||||
|
|
||||||
Rojo uses two related libraries to deserialize instances from Roblox's file formats, `rbx_xml` and `rbx_binary`.
|
|
||||||
|
|
||||||
### In-Memory Filesystem (IMFS)
|
|
||||||
Relevant source files:
|
|
||||||
|
|
||||||
* [`server/src/imfs.rs`](https://github.com/LPGhatguy/rojo/blob/master/server/src/imfs.rs)
|
|
||||||
* [`server/src/fs_watcher.rs`](https://github.com/LPGhatguy/rojo/blob/master/server/src/fs_watcher.rs)
|
|
||||||
|
|
||||||
Rojo keeps an in-memory copy of all files that it needs reasons about. This enables taking fast, stateless, tear-tree snapshots of files to turn them into instances.
|
|
||||||
|
|
||||||
Keeping an in-memory copy of file contents will also enable Rojo to debounce changes that are caused by Rojo itself. This'll happen when two-way sync finally happens.
|
|
||||||
|
|
||||||
### Snapshot Reconciler
|
|
||||||
Relevant source files:
|
|
||||||
|
|
||||||
* [`server/src/snapshot_reconciler.rs`](https://github.com/LPGhatguy/rojo/blob/master/server/src/snapshot_reconciler.rs)
|
|
||||||
* [`server/src/rbx_snapshot.rs`](https://github.com/LPGhatguy/rojo/blob/master/server/src/rbx_snapshot.rs)
|
|
||||||
* [`server/src/rbx_session.rs`](https://github.com/LPGhatguy/rojo/blob/master/server/src/rbx_session.rs)
|
|
||||||
|
|
||||||
To simplify incremental updates of instances, Rojo generates lightweight snapshots describing how files map to instances. This means that Rojo can treat file change events similarly to damage painting as opposed to trying to surgically update the correct instances.
|
|
||||||
|
|
||||||
This approach reduces the number of desynchronization bugs, reduces the complexity of important pieces of the codebase, and makes writing plugins a lot easier.
|
|
||||||
|
|
||||||
### HTTP API
|
|
||||||
Relevant source files:
|
|
||||||
|
|
||||||
* [`server/src/web.rs`](https://github.com/LPGhatguy/rojo/blob/master/server/src/web.rs)
|
|
||||||
|
|
||||||
The Rojo live-sync server and Roblox Studio plugin communicate via HTTP.
|
|
||||||
|
|
||||||
Requests sent from the plugin to the server are regular HTTP requests.
|
|
||||||
|
|
||||||
Messages sent from the server to the plugin are delivered via HTTP long-polling. This is an approach that uses long-lived HTTP requests that restart on timeout. It's largely been replaced by WebSockets, but Roblox doesn't have support for them.
|
|
||||||
|
|
||||||
## Roblox Studio Plugin
|
|
||||||
TODO
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
Rojo underwent a large refactor during most of 2018 to enable a bunch of new features and lay groundwork for lots more in 2019. As such, Rojo **0.5.x** projects are not compatible with Rojo **0.4.x** projects.
|
|
||||||
|
|
||||||
[TOC]
|
|
||||||
|
|
||||||
## Supporting Both 0.4.x and 0.5.x
|
|
||||||
Rojo 0.5.x uses a different name for its project format. While 0.4.x used `rojo.json`, 0.5.x uses `default.project.json`, which allows them to coexist.
|
|
||||||
|
|
||||||
If you aren't sure about upgrading or want to upgrade gradually, it's possible to keep both files in the same project without causing problems.
|
|
||||||
|
|
||||||
## Upgrading Your Project File
|
|
||||||
Project files in 0.5.x are more explicit and flexible than they were in 0.4.x. Project files can now describe models and plugins in addition to places.
|
|
||||||
|
|
||||||
This new project file format also guards against two of the biggest pitfalls when writing a config file:
|
|
||||||
|
|
||||||
* Using a service as a partition target directly, which often wiped away extra instances
|
|
||||||
* Defining two partitions that overlapped, which made Rojo act unpredictably
|
|
||||||
|
|
||||||
The biggest change is that the `partitions` field has been replaced with a new field, `tree`, that describes the entire hierarchy of your project from the top-down.
|
|
||||||
|
|
||||||
A project for 0.4.x that syncs from the `src` directory into `ReplicatedStorage.Source` would look like this:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Rojo 0.4.x Example",
|
|
||||||
"partitions": {
|
|
||||||
"path": "src",
|
|
||||||
"target": "ReplicatedStorage.Source"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
In 0.5.x, the project format is more explicit:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Rojo 0.5.x Example",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
"ReplicatedStorage": {
|
|
||||||
"$className": "ReplicatedStorage",
|
|
||||||
"Source": {
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For each object in the tree, we define *metadata* and *children*.
|
|
||||||
|
|
||||||
Metadata begins with a dollar sign (`$`), like `$className`. This is so that children and metadata can coexist without creating too many nested layers.
|
|
||||||
|
|
||||||
All other values are considered children, where the key is the instance's name, and the value is an object, repeating the process.
|
|
||||||
|
|
||||||
## Migrating Unknown Files
|
|
||||||
If you used Rojo to sync in files as `StringValue` objects, you'll need to make sure those files end with the `txt` extension to preserve this in Rojo 0.5.x.
|
|
||||||
|
|
||||||
Unknown files are now ignored in Rojo instead of being converted to `StringValue` objects.
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
[TOC]
|
|
||||||
|
|
||||||
## Project File
|
|
||||||
|
|
||||||
Rojo projects are JSON files that have the `.project.json` extension. They have these fields:
|
|
||||||
|
|
||||||
* `name`: A string indicating the name of the project.
|
|
||||||
* This is only used for diagnostics.
|
|
||||||
* `tree`: An [Instance Description](#instance-description) describing the root instance of the project.
|
|
||||||
|
|
||||||
## Instance Description
|
|
||||||
Instance Descriptions correspond one-to-one with the actual Roblox Instances in the project. They can be specified directly in the project file or be pulled from the filesystem.
|
|
||||||
|
|
||||||
* `$className`: The ClassName of the Instance being described.
|
|
||||||
* Optional if `$path` is specified.
|
|
||||||
* `$path`: The path on the filesystem to pull files from into the project.
|
|
||||||
* Optional if `$className` is specified.
|
|
||||||
* Paths are relative to the folder containing the project file.
|
|
||||||
* `$properties`: Properties to apply to the instance. Values should be [Instance Property Values](#instance-property-value).
|
|
||||||
* Optional
|
|
||||||
* `$ignoreUnknownInstances`: Whether instances that Rojo doesn't know about should be deleted.
|
|
||||||
* Optional
|
|
||||||
* Default is `false` if `$path` is specified, otherwise `true`.
|
|
||||||
|
|
||||||
All other fields in an Instance Description are turned into instances whose name is the key. These values should also be Instance Descriptions!
|
|
||||||
|
|
||||||
Instance Descriptions are fairly verbose and strict. In the future, it'll be possible for Rojo to infer class names for known services like `Workspace`.
|
|
||||||
|
|
||||||
## Instance Property Value
|
|
||||||
The shape of Instance Property Values is defined by the [rbx_tree](https://github.com/LPGhatguy/rbx-tree) library, so it uses slightly different conventions than the rest of Rojo.
|
|
||||||
|
|
||||||
Each value should be an object with the following required fields:
|
|
||||||
|
|
||||||
* `Type`: The type of property to represent.
|
|
||||||
* [Supported types can be found here](https://github.com/LPGhatguy/rbx-tree#property-type-coverage).
|
|
||||||
* `Value`: The value of the property.
|
|
||||||
* The shape of this field depends on which property type is being used. `Vector3` and `Color3` values are both represented as a list of numbers, for example.
|
|
||||||
|
|
||||||
Instance Property Values are intentionally very strict. Rojo will eventually be able to infer types for you!
|
|
||||||
|
|
||||||
## Example Projects
|
|
||||||
This project bundles up everything in the `src` directory. It'd be suitable for making a plugin or model:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "AwesomeLibrary",
|
|
||||||
"tree": {
|
|
||||||
"$path": "src"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This project describes the layout you might use if you were making the next hit simulator game, *Sisyphus Simulator*:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "Sisyphus Simulator",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
|
|
||||||
"HttpService": {
|
|
||||||
"$className": "HttpService",
|
|
||||||
"$properties": {
|
|
||||||
"HttpEnabled": {
|
|
||||||
"Type": "Bool",
|
|
||||||
"Value": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"ReplicatedStorage": {
|
|
||||||
"$className": "ReplicatedStorage",
|
|
||||||
"$path": "src/ReplicatedStorage"
|
|
||||||
},
|
|
||||||
|
|
||||||
"StarterPlayer": {
|
|
||||||
"$className": "StarterPlayer",
|
|
||||||
|
|
||||||
"StarterPlayerScripts": {
|
|
||||||
"$className": "StarterPlayerScripts",
|
|
||||||
"$path": "src/StarterPlayerScripts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"Workspace": {
|
|
||||||
"$className": "Workspace",
|
|
||||||
"$properties": {
|
|
||||||
"Gravity": {
|
|
||||||
"Type": "Float32",
|
|
||||||
"Value": 67.3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"Terrain": {
|
|
||||||
"$path": "Terrain.rbxm"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
mkdocs
|
|
||||||
mkdocs-material
|
|
||||||
pymdown-extensions
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
This page aims to describe how Rojo turns files on the filesystem into Roblox objects.
|
|
||||||
|
|
||||||
[TOC]
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
| File Name | Instance Type |
|
|
||||||
| -------------- | ------------------- |
|
|
||||||
| any directory | `Folder` |
|
|
||||||
| `*.server.lua` | `Script` |
|
|
||||||
| `*.client.lua` | `LocalScript` |
|
|
||||||
| `*.lua` | `ModuleScript` |
|
|
||||||
| `*.csv` | `LocalizationTable` |
|
|
||||||
| `*.txt` | `StringValue` |
|
|
||||||
| `*.model.json` | Any |
|
|
||||||
| `*.rbxm` | Any |
|
|
||||||
| `*.rbxmx` | Any |
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
Not all property types can be synced by Rojo in real-time due to limitations of the Roblox Studio plugin API. In these cases, you can usually generate a place file and open it when you start working on a project.
|
|
||||||
|
|
||||||
Some common cases you might hit are:
|
|
||||||
|
|
||||||
* Binary data (Terrain, CSG, CollectionService tags)
|
|
||||||
* `MeshPart.MeshId`
|
|
||||||
* `HttpService.HttpEnabled`
|
|
||||||
|
|
||||||
For a list of all property types that Rojo can reason about, both when live-syncing and when building place files, look at [rbx_tree's type coverage chart](https://github.com/LPGhatguy/rbx-tree#property-type-coverage).
|
|
||||||
|
|
||||||
## Folders
|
|
||||||
Any directory on the filesystem will turn into a `Folder` instance unless it contains an 'init' script, described below.
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
The default script type in Rojo projects is `ModuleScript`, since most scripts in well-structued Roblox projects will be modules.
|
|
||||||
|
|
||||||
If a directory contains a file named `init.server.lua`, `init.client.lua`, or `init.lua`, that folder will be transformed into a `*Script` instance with the contents of the 'init' file. This can be used to create scripts inside of scripts.
|
|
||||||
|
|
||||||
For example, these files:
|
|
||||||
|
|
||||||

|
|
||||||
{: align="center" }
|
|
||||||
|
|
||||||
Will turn into these instances in Roblox:
|
|
||||||
|
|
||||||

|
|
||||||
{: align="center" }
|
|
||||||
|
|
||||||
## Localization Tables
|
|
||||||
Any CSV files are transformed into `LocalizationTable` instances. Rojo expects these files to follow the same format that Roblox does when importing and exporting localization information.
|
|
||||||
|
|
||||||
## Plain Text Files
|
|
||||||
Plain text files (`.txt`) files are transformed into `StringValue` instances. This is useful for bringing in text data that can be read by scripts at runtime.
|
|
||||||
|
|
||||||
## JSON Models
|
|
||||||
Files ending in `.model.json` can be used to describe simple models. They're designed to be hand-written and are useful for instances like `RemoteEvent`.
|
|
||||||
|
|
||||||
A JSON model describing a folder containing a `Part` and a `RemoteEvent` could be described as:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Name": "My Cool Model",
|
|
||||||
"ClassName": "Folder",
|
|
||||||
"Children": [
|
|
||||||
{
|
|
||||||
"Name": "RootPart",
|
|
||||||
"ClassName": "Part",
|
|
||||||
"Properties": {
|
|
||||||
"Size": {
|
|
||||||
"Type": "Vector3",
|
|
||||||
"Value": [4, 4, 4]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "SendMoney",
|
|
||||||
"ClassName": "RemoteEvent"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
It would turn into instances in this shape:
|
|
||||||
|
|
||||||

|
|
||||||
{: align="center" }
|
|
||||||
|
|
||||||
## Binary and XML Models
|
|
||||||
Rojo supports both binary (`.rbxm`) and XML (`.rbxmx`) models generated by Roblox Studio or another tool.
|
|
||||||
|
|
||||||
Not all property types are supported for all formats!
|
|
||||||
|
|
||||||
For a rundown of supported types, check out [rbx_tree's type coverage chart](https://github.com/LPGhatguy/rbx-tree#property-type-coverage).
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
There are a number of existing plugins for Roblox that move code from the filesystem into Roblox.
|
|
||||||
|
|
||||||
Besides Rojo, you might consider:
|
|
||||||
|
|
||||||
* [rbxmk by Anaminus](https://github.com/anaminus/rbxmk)
|
|
||||||
* [Rofresh by Osyris](https://github.com/osyrisrblx/rofresh)
|
|
||||||
* [RbxRefresh by Osyris](https://github.com/osyrisrblx/RbxRefresh)
|
|
||||||
* [Studio Bridge by Vocksel](https://github.com/vocksel/studio-bridge)
|
|
||||||
* [Elixir by Vocksel](https://github.com/vocksel/elixir)
|
|
||||||
* [RbxSync by evaera](https://github.com/evaera/RbxSync)
|
|
||||||
* [CodeSync by MemoryPenguin](https://github.com/MemoryPenguin/CodeSync)
|
|
||||||
* [rbx-exteditor by MemoryPenguin](https://github.com/MemoryPenguin/rbx-exteditor)
|
|
||||||
|
|
||||||
So why did I build Rojo?
|
|
||||||
|
|
||||||
Each of these tools solves what is essentially the same problem from a few different angles. The goal of Rojo is to take all of the lessons and ideas learned from these projects and build a tool that can solve this problem for good.
|
|
||||||
|
|
||||||
Additionally:
|
|
||||||
|
|
||||||
* I think that this tool needs to be built in a compiled language without a runtime, for easy distribution and good performance.
|
|
||||||
* I think that the conventions promoted by other sync plugins (`.module.lua` for modules, as well a single sync point) are sub-optimal.
|
|
||||||
* I think that I have a good enough understanding of the problem to build something robust.
|
|
||||||
* I think that Rojo should be able to do more than just sync code.
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Kludged documentation generator to support multiple versions.
|
|
||||||
# Make sure the `site` folder is a checkout of this repository's `gh-pages`
|
|
||||||
# branch.
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
REMOTE=$(git remote get-url origin)
|
|
||||||
CHECKOUT="$(mktemp -d)"
|
|
||||||
OUTPUT="$(pwd)/site"
|
|
||||||
|
|
||||||
if [ -d site ]
|
|
||||||
then
|
|
||||||
cd site
|
|
||||||
git pull
|
|
||||||
else
|
|
||||||
git clone "$REMOTE" site
|
|
||||||
cd site
|
|
||||||
git checkout gh-pages
|
|
||||||
fi
|
|
||||||
|
|
||||||
git clone "$REMOTE" "$CHECKOUT"
|
|
||||||
cd "$CHECKOUT"
|
|
||||||
|
|
||||||
echo "Building master"
|
|
||||||
git checkout master
|
|
||||||
mkdocs build --site-dir "$OUTPUT"
|
|
||||||
|
|
||||||
echo "Building 0.5.x"
|
|
||||||
mkdocs build --site-dir "$OUTPUT/0.5.x"
|
|
||||||
|
|
||||||
echo "Building 0.4.x"
|
|
||||||
git checkout v0.4.x
|
|
||||||
mkdocs build --site-dir "$OUTPUT/0.4.x"
|
|
||||||
31
mkdocs.yml
@@ -1,31 +0,0 @@
|
|||||||
site_name: Rojo Documentation
|
|
||||||
repo_name: LPGhatguy/rojo
|
|
||||||
repo_url: https://github.com/LPGhatguy/rojo
|
|
||||||
|
|
||||||
theme:
|
|
||||||
name: material
|
|
||||||
palette:
|
|
||||||
primary: 'Red'
|
|
||||||
accent: 'Red'
|
|
||||||
|
|
||||||
nav:
|
|
||||||
- Home: index.md
|
|
||||||
- Why Rojo?: why-rojo.md
|
|
||||||
- Installation: installation.md
|
|
||||||
- Creating a Place with Rojo: creating-a-place.md
|
|
||||||
- Migrating from 0.4.x to 0.5.x: migrating-to-epiphany.md
|
|
||||||
- Project Format: project-format.md
|
|
||||||
- Sync Details: sync-details.md
|
|
||||||
- Rojo Internals:
|
|
||||||
- Internals Overview: internals/overview.md
|
|
||||||
|
|
||||||
extra_css:
|
|
||||||
- extra.css
|
|
||||||
|
|
||||||
markdown_extensions:
|
|
||||||
- attr_list
|
|
||||||
- admonition
|
|
||||||
- codehilite:
|
|
||||||
guess_lang: false
|
|
||||||
- toc:
|
|
||||||
permalink: true
|
|
||||||
1
plugin/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
/luacov.*
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
return {
|
|
||||||
include = {
|
|
||||||
"^src",
|
|
||||||
},
|
|
||||||
exclude = {
|
|
||||||
"%.spec$",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Rojo Plugin
|
|
||||||
|
|
||||||
This is the source to the Rojo Roblox Studio plugin.
|
|
||||||
|
|
||||||
Documentation is WIP.
|
|
||||||
@@ -5,17 +5,26 @@
|
|||||||
"Plugin": {
|
"Plugin": {
|
||||||
"$path": "src"
|
"$path": "src"
|
||||||
},
|
},
|
||||||
|
"Log": {
|
||||||
|
"$path": "log"
|
||||||
|
},
|
||||||
|
"Http": {
|
||||||
|
"$path": "http"
|
||||||
|
},
|
||||||
|
"Fmt": {
|
||||||
|
"$path": "fmt"
|
||||||
|
},
|
||||||
"Roact": {
|
"Roact": {
|
||||||
"$path": "modules/roact/lib"
|
"$path": "modules/roact/src"
|
||||||
},
|
|
||||||
"Rodux": {
|
|
||||||
"$path": "modules/rodux/lib"
|
|
||||||
},
|
|
||||||
"RoactRodux": {
|
|
||||||
"$path": "modules/roact-rodux/lib"
|
|
||||||
},
|
},
|
||||||
"Promise": {
|
"Promise": {
|
||||||
"$path": "modules/promise/lib"
|
"$path": "modules/promise/lib"
|
||||||
|
},
|
||||||
|
"t": {
|
||||||
|
"$path": "modules/t/lib"
|
||||||
|
},
|
||||||
|
"RbxDom": {
|
||||||
|
"$path": "modules/rbx-dom/rbx_dom_lua/src"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
245
plugin/fmt/init.lua
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
--[[
|
||||||
|
This library describes a formatting mechanism akin to Rust's std::fmt.
|
||||||
|
|
||||||
|
It has a couple building blocks:
|
||||||
|
|
||||||
|
* A new syntax for formatting strings, taken verbatim from Rust. It'd also
|
||||||
|
be possible to use printf-style formatting specifiers to integrate with
|
||||||
|
the existing string.format utility.
|
||||||
|
|
||||||
|
* An equivalent to Rust's `Display` trait. We're mapping the semantics of
|
||||||
|
tostring and the __tostring metamethod onto this trait. A lot of types
|
||||||
|
should already have __tostring implementations, too!
|
||||||
|
|
||||||
|
* An equivalent to Rust's `Debug` trait. This library Lua-ifies that idea by
|
||||||
|
inventing a new metamethod, `__fmtDebug`. We pass along the "extended
|
||||||
|
form" attribute which is the equivalent of the "alternate mode" in Rust's
|
||||||
|
Debug trait since it's the author's opinion that treating it as a
|
||||||
|
verbosity flag is semantically accurate.
|
||||||
|
]]
|
||||||
|
|
||||||
|
--[[
|
||||||
|
The default implementation of __fmtDebug for tables when the extended option
|
||||||
|
is not set.
|
||||||
|
]]
|
||||||
|
local function defaultTableDebug(buffer, input)
|
||||||
|
buffer:writeRaw("{")
|
||||||
|
|
||||||
|
for key, value in pairs(input) do
|
||||||
|
buffer:write("[{:?}] = {:?}", key, value)
|
||||||
|
|
||||||
|
if next(input, key) ~= nil then
|
||||||
|
buffer:writeRaw(", ")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
buffer:writeRaw("}")
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
The default implementation of __fmtDebug for tables with the extended option
|
||||||
|
set.
|
||||||
|
]]
|
||||||
|
local function defaultTableDebugExtended(buffer, input)
|
||||||
|
-- Special case for empty tables.
|
||||||
|
if next(input) == nil then
|
||||||
|
buffer:writeRaw("{}")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
buffer:writeLineRaw("{")
|
||||||
|
buffer:indent()
|
||||||
|
|
||||||
|
for key, value in pairs(input) do
|
||||||
|
buffer:writeLine("[{:?}] = {:#?},", key, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
buffer:unindent()
|
||||||
|
buffer:writeRaw("}")
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
The default debug representation for all types.
|
||||||
|
]]
|
||||||
|
local function debugImpl(buffer, value, extendedForm)
|
||||||
|
local valueType = typeof(value)
|
||||||
|
|
||||||
|
if valueType == "string" then
|
||||||
|
local formatted = string.format("%q", value)
|
||||||
|
buffer:writeRaw(formatted)
|
||||||
|
elseif valueType == "table" then
|
||||||
|
local valueMeta = getmetatable(value)
|
||||||
|
|
||||||
|
if valueMeta ~= nil and valueMeta.__fmtDebug ~= nil then
|
||||||
|
-- This type implement's the metamethod we made up to line up with
|
||||||
|
-- Rust's 'Debug' trait.
|
||||||
|
|
||||||
|
valueMeta.__fmtDebug(value, buffer, extendedForm)
|
||||||
|
else
|
||||||
|
if extendedForm then
|
||||||
|
defaultTableDebugExtended(buffer, value)
|
||||||
|
else
|
||||||
|
defaultTableDebug(buffer, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif valueType == "Instance" then
|
||||||
|
buffer:writeRaw(value:GetFullName())
|
||||||
|
else
|
||||||
|
buffer:writeRaw(tostring(value))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Defines and implements the library's template syntax.
|
||||||
|
]]
|
||||||
|
local function writeFmt(buffer, template, ...)
|
||||||
|
local currentArg = 0
|
||||||
|
local i = 1
|
||||||
|
local len = #template
|
||||||
|
|
||||||
|
while i <= len do
|
||||||
|
local openBrace = template:find("{", i)
|
||||||
|
|
||||||
|
if openBrace == nil then
|
||||||
|
-- There are no remaining open braces in this string, so we can
|
||||||
|
-- write the rest of the string to the buffer.
|
||||||
|
|
||||||
|
buffer:writeRaw(template:sub(i))
|
||||||
|
break
|
||||||
|
else
|
||||||
|
-- We found an open brace! This could be:
|
||||||
|
-- - A literal '{', written as '{{'
|
||||||
|
-- - The beginning of an interpolation, like '{}'
|
||||||
|
-- - An error, if there's no matching '}'
|
||||||
|
|
||||||
|
local charAfterBrace = template:sub(openBrace + 1, openBrace + 1)
|
||||||
|
if charAfterBrace == "{" then
|
||||||
|
-- This is a literal brace, so we'll write everything up to this
|
||||||
|
-- point (including the first brace), and then skip over the
|
||||||
|
-- second brace.
|
||||||
|
|
||||||
|
buffer:writeRaw(template:sub(i, openBrace))
|
||||||
|
i = openBrace + 2
|
||||||
|
else
|
||||||
|
-- This SHOULD be an interpolation. We'll find our matching
|
||||||
|
-- brace and treat the contents as the formatting specifier.
|
||||||
|
|
||||||
|
-- If there were any unwritten characters before this
|
||||||
|
-- interpolation, write them to the buffer.
|
||||||
|
if openBrace - i > 0 then
|
||||||
|
buffer:writeRaw(template:sub(i, openBrace - 1))
|
||||||
|
end
|
||||||
|
|
||||||
|
local closeBrace = template:find("}", openBrace + 1)
|
||||||
|
assert(closeBrace ~= nil, "Unclosed formatting specifier. Use '{{' to write an open brace.")
|
||||||
|
|
||||||
|
local formatSpecifier = template:sub(openBrace + 1, closeBrace - 1)
|
||||||
|
currentArg = currentArg + 1
|
||||||
|
local arg = select(currentArg, ...)
|
||||||
|
|
||||||
|
if formatSpecifier == "" then
|
||||||
|
-- This should use the equivalent of Rust's 'Display', ie
|
||||||
|
-- tostring and the __tostring metamethod.
|
||||||
|
|
||||||
|
buffer:writeRaw(tostring(arg))
|
||||||
|
elseif formatSpecifier == ":?" then
|
||||||
|
-- This should use the equivalent of Rust's 'Debug',
|
||||||
|
-- invented for this library as __fmtDebug.
|
||||||
|
|
||||||
|
debugImpl(buffer, arg, false)
|
||||||
|
elseif formatSpecifier == ":#?" then
|
||||||
|
-- This should use the equivlant of Rust's 'Debug' with the
|
||||||
|
-- 'alternate' (ie expanded) flag set.
|
||||||
|
|
||||||
|
debugImpl(buffer, arg, true)
|
||||||
|
else
|
||||||
|
error("unsupported format specifier " .. formatSpecifier, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
i = closeBrace + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function debugOutputBuffer()
|
||||||
|
local buffer = {}
|
||||||
|
local startOfLine = true
|
||||||
|
local indentLevel = 0
|
||||||
|
local indentation = ""
|
||||||
|
|
||||||
|
function buffer:writeLine(template, ...)
|
||||||
|
writeFmt(self, template, ...)
|
||||||
|
self:nextLine()
|
||||||
|
end
|
||||||
|
|
||||||
|
function buffer:writeLineRaw(value)
|
||||||
|
self:writeRaw(value)
|
||||||
|
self:nextLine()
|
||||||
|
end
|
||||||
|
|
||||||
|
function buffer:write(template, ...)
|
||||||
|
return writeFmt(self, template, ...)
|
||||||
|
end
|
||||||
|
|
||||||
|
function buffer:writeRaw(value)
|
||||||
|
if #value > 0 then
|
||||||
|
if startOfLine and #indentation > 0 then
|
||||||
|
startOfLine = false
|
||||||
|
table.insert(self, indentation)
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(self, value)
|
||||||
|
startOfLine = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function buffer:nextLine()
|
||||||
|
table.insert(self, "\n")
|
||||||
|
startOfLine = true
|
||||||
|
end
|
||||||
|
|
||||||
|
function buffer:indent()
|
||||||
|
indentLevel = indentLevel + 1
|
||||||
|
indentation = string.rep(" ", indentLevel)
|
||||||
|
end
|
||||||
|
|
||||||
|
function buffer:unindent()
|
||||||
|
indentLevel = math.max(0, indentLevel - 1)
|
||||||
|
indentation = string.rep(" ", indentLevel)
|
||||||
|
end
|
||||||
|
|
||||||
|
function buffer:finish()
|
||||||
|
return table.concat(self, "")
|
||||||
|
end
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fmt(template, ...)
|
||||||
|
local buffer = debugOutputBuffer()
|
||||||
|
writeFmt(buffer, template, ...)
|
||||||
|
return buffer:finish()
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Wrap the given object in a type that implements the given function as its
|
||||||
|
Debug implementation, and forwards __tostring to the type's underlying
|
||||||
|
tostring implementation.
|
||||||
|
]]
|
||||||
|
local function debugify(object, fmtFunc)
|
||||||
|
return setmetatable({}, {
|
||||||
|
__fmtDebug = function(_, ...)
|
||||||
|
return fmtFunc(object, ...)
|
||||||
|
end,
|
||||||
|
__tostring = function()
|
||||||
|
return tostring(object)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
debugOutputBuffer = debugOutputBuffer,
|
||||||
|
fmt = fmt,
|
||||||
|
debugify = debugify,
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
local Logging = require(script.Parent.Logging)
|
local Error = {}
|
||||||
|
Error.__index = Error
|
||||||
|
|
||||||
local HttpError = {}
|
Error.Kind = {
|
||||||
HttpError.__index = HttpError
|
|
||||||
|
|
||||||
HttpError.Error = {
|
|
||||||
HttpNotEnabled = {
|
HttpNotEnabled = {
|
||||||
message = "Rojo requires HTTP access, which is not enabled.\n" ..
|
message = "Rojo requires HTTP access, which is not enabled.\n" ..
|
||||||
"Check your game settings, located in the 'Home' tab of Studio.",
|
"Check your game settings, located in the 'Home' tab of Studio.",
|
||||||
@@ -13,20 +11,20 @@ HttpError.Error = {
|
|||||||
"Make sure the server is running -- use 'rojo serve' to run it!",
|
"Make sure the server is running -- use 'rojo serve' to run it!",
|
||||||
},
|
},
|
||||||
Timeout = {
|
Timeout = {
|
||||||
message = "Request timed out.",
|
message = "HTTP request timed out.",
|
||||||
},
|
},
|
||||||
Unknown = {
|
Unknown = {
|
||||||
message = "Unknown error: {{message}}",
|
message = "Unknown HTTP error: {{message}}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
setmetatable(HttpError.Error, {
|
setmetatable(Error.Kind, {
|
||||||
__index = function(_, key)
|
__index = function(_, key)
|
||||||
error(("%q is not a valid member of HttpError.Error"):format(tostring(key)), 2)
|
error(("%q is not a valid member of Http.Error.Kind"):format(tostring(key)), 2)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
function HttpError.new(type, extraMessage)
|
function Error.new(type, extraMessage)
|
||||||
extraMessage = extraMessage or ""
|
extraMessage = extraMessage or ""
|
||||||
local message = type.message:gsub("{{message}}", extraMessage)
|
local message = type.message:gsub("{{message}}", extraMessage)
|
||||||
|
|
||||||
@@ -35,38 +33,34 @@ function HttpError.new(type, extraMessage)
|
|||||||
message = message,
|
message = message,
|
||||||
}
|
}
|
||||||
|
|
||||||
setmetatable(err, HttpError)
|
setmetatable(err, Error)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
end
|
end
|
||||||
|
|
||||||
function HttpError:__tostring()
|
function Error:__tostring()
|
||||||
return self.message
|
return self.message
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
This method shouldn't have to exist. Ugh.
|
This method shouldn't have to exist. Ugh.
|
||||||
]]
|
]]
|
||||||
function HttpError.fromErrorString(message)
|
function Error.fromRobloxErrorString(message)
|
||||||
local lower = message:lower()
|
local lower = message:lower()
|
||||||
|
|
||||||
if lower:find("^http requests are not enabled") then
|
if lower:find("^http requests are not enabled") then
|
||||||
return HttpError.new(HttpError.Error.HttpNotEnabled)
|
return Error.new(Error.Kind.HttpNotEnabled)
|
||||||
end
|
end
|
||||||
|
|
||||||
if lower:find("^httperror: timedout") then
|
if lower:find("^httperror: timedout") then
|
||||||
return HttpError.new(HttpError.Error.Timeout)
|
return Error.new(Error.Kind.Timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
if lower:find("^httperror: connectfail") then
|
if lower:find("^httperror: connectfail") then
|
||||||
return HttpError.new(HttpError.Error.ConnectFailed)
|
return Error.new(Error.Kind.ConnectFailed)
|
||||||
end
|
end
|
||||||
|
|
||||||
return HttpError.new(HttpError.Error.Unknown, message)
|
return Error.new(Error.Kind.Unknown, message)
|
||||||
end
|
end
|
||||||
|
|
||||||
function HttpError:report()
|
return Error
|
||||||
Logging.warn(self.message)
|
|
||||||
end
|
|
||||||
|
|
||||||
return HttpError
|
|
||||||
@@ -1,34 +1,34 @@
|
|||||||
local HttpService = game:GetService("HttpService")
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
local stringTemplate = [[
|
local stringTemplate = [[
|
||||||
HttpResponse {
|
Http.Response {
|
||||||
code: %d
|
code: %d
|
||||||
body: %s
|
body: %s
|
||||||
}]]
|
}]]
|
||||||
|
|
||||||
local HttpResponse = {}
|
local Response = {}
|
||||||
HttpResponse.__index = HttpResponse
|
Response.__index = Response
|
||||||
|
|
||||||
function HttpResponse:__tostring()
|
function Response:__tostring()
|
||||||
return stringTemplate:format(self.code, self.body)
|
return stringTemplate:format(self.code, self.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
function HttpResponse.fromRobloxResponse(response)
|
function Response.fromRobloxResponse(response)
|
||||||
local self = {
|
local self = {
|
||||||
body = response.Body,
|
body = response.Body,
|
||||||
code = response.StatusCode,
|
code = response.StatusCode,
|
||||||
headers = response.Headers,
|
headers = response.Headers,
|
||||||
}
|
}
|
||||||
|
|
||||||
return setmetatable(self, HttpResponse)
|
return setmetatable(self, Response)
|
||||||
end
|
end
|
||||||
|
|
||||||
function HttpResponse:isSuccess()
|
function Response:isSuccess()
|
||||||
return self.code >= 200 and self.code < 300
|
return self.code >= 200 and self.code < 300
|
||||||
end
|
end
|
||||||
|
|
||||||
function HttpResponse:json()
|
function Response:json()
|
||||||
return HttpService:JSONDecode(self.body)
|
return HttpService:JSONDecode(self.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
return HttpResponse
|
return Response
|
||||||
66
plugin/http/init.lua
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
local HttpService = game:GetService("HttpService")
|
||||||
|
|
||||||
|
local Promise = require(script.Parent.Promise)
|
||||||
|
local Log = require(script.Parent.Log)
|
||||||
|
|
||||||
|
local HttpError = require(script.Error)
|
||||||
|
local HttpResponse = require(script.Response)
|
||||||
|
|
||||||
|
local lastRequestId = 0
|
||||||
|
|
||||||
|
local Http = {}
|
||||||
|
|
||||||
|
Http.Error = HttpError
|
||||||
|
Http.Response = HttpResponse
|
||||||
|
|
||||||
|
local function performRequest(requestParams)
|
||||||
|
local requestId = lastRequestId + 1
|
||||||
|
lastRequestId = requestId
|
||||||
|
|
||||||
|
Log.trace("HTTP {}({}) {}", requestParams.Method, requestId, requestParams.Url)
|
||||||
|
|
||||||
|
if requestParams.Body ~= nil then
|
||||||
|
Log.trace("{}", requestParams.Body)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Promise.new(function(resolve, reject)
|
||||||
|
coroutine.wrap(function()
|
||||||
|
local success, response = pcall(function()
|
||||||
|
return HttpService:RequestAsync(requestParams)
|
||||||
|
end)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
Log.trace("Request {} success, status code {}", requestId, response.StatusCode)
|
||||||
|
resolve(HttpResponse.fromRobloxResponse(response))
|
||||||
|
else
|
||||||
|
Log.trace("Request {} failure: {:?}", requestId, response)
|
||||||
|
reject(HttpError.fromRobloxErrorString(response))
|
||||||
|
end
|
||||||
|
end)()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Http.get(url)
|
||||||
|
return performRequest({
|
||||||
|
Url = url,
|
||||||
|
Method = "GET",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function Http.post(url, body)
|
||||||
|
return performRequest({
|
||||||
|
Url = url,
|
||||||
|
Method = "POST",
|
||||||
|
Body = body,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function Http.jsonEncode(object)
|
||||||
|
return HttpService:JSONEncode(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Http.jsonDecode(source)
|
||||||
|
return HttpService:JSONDecode(source)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Http
|
||||||
5
plugin/http/init.spec.lua
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
return function()
|
||||||
|
it("should load", function()
|
||||||
|
require(script.Parent)
|
||||||
|
end)
|
||||||
|
end
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
--[[
|
|
||||||
Loads the Rojo plugin and all of its dependencies.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local function loadEnvironment()
|
|
||||||
-- If you add any dependencies, add them to this table so they'll be loaded!
|
|
||||||
local LOAD_MODULES = {
|
|
||||||
{"src", "Rojo"},
|
|
||||||
{"modules/promise/lib", "Promise"},
|
|
||||||
{"modules/testez/lib", "TestEZ"},
|
|
||||||
}
|
|
||||||
|
|
||||||
-- This makes sure we can load Lemur and other libraries that depend on init.lua
|
|
||||||
package.path = package.path .. ";?/init.lua"
|
|
||||||
|
|
||||||
-- If this fails, make sure you've run `lua bin/install-dependencies.lua` first!
|
|
||||||
local lemur = require("modules.lemur")
|
|
||||||
|
|
||||||
-- Create a virtual Roblox tree
|
|
||||||
local habitat = lemur.Habitat.new()
|
|
||||||
|
|
||||||
-- We'll put all of our library code and dependencies here
|
|
||||||
local modules = lemur.Instance.new("Folder")
|
|
||||||
modules.Name = "Modules"
|
|
||||||
modules.Parent = habitat.game:GetService("ReplicatedStorage")
|
|
||||||
|
|
||||||
-- Load all of the modules specified above
|
|
||||||
for _, module in ipairs(LOAD_MODULES) do
|
|
||||||
local container = habitat:loadFromFs(module[1])
|
|
||||||
container.Name = module[2]
|
|
||||||
container.Parent = modules
|
|
||||||
end
|
|
||||||
|
|
||||||
return habitat, modules
|
|
||||||
end
|
|
||||||
|
|
||||||
return loadEnvironment
|
|
||||||
@@ -1,49 +1,55 @@
|
|||||||
local DevSettings = require(script.Parent.DevSettings)
|
local Fmt = require(script.Parent.Fmt)
|
||||||
|
|
||||||
local Level = {
|
local Level = {
|
||||||
Error = 0,
|
Error = 0,
|
||||||
Warning = 1,
|
Warning = 1,
|
||||||
Info = 2,
|
Info = 2,
|
||||||
Trace = 3,
|
Debug = 3,
|
||||||
|
Trace = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
local testLogLevel = nil
|
|
||||||
|
|
||||||
local function getLogLevel()
|
local function getLogLevel()
|
||||||
if testLogLevel ~= nil then
|
return Level.Info
|
||||||
return testLogLevel
|
|
||||||
end
|
|
||||||
|
|
||||||
return DevSettings:getLogLevel()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function addTags(tag, message)
|
local function addTags(tag, message)
|
||||||
return tag .. message:gsub("\n", "\n" .. tag)
|
return tag .. message:gsub("\n", "\n" .. tag)
|
||||||
end
|
end
|
||||||
|
|
||||||
local INFO_TAG = (" "):rep(15) .. "[Rojo-Info] "
|
|
||||||
local TRACE_TAG = (" "):rep(15) .. "[Rojo-Trace] "
|
local TRACE_TAG = (" "):rep(15) .. "[Rojo-Trace] "
|
||||||
|
local INFO_TAG = (" "):rep(15) .. "[Rojo-Info] "
|
||||||
|
local DEBUG_TAG = (" "):rep(15) .. "[Rojo-Debug] "
|
||||||
local WARN_TAG = "[Rojo-Warn] "
|
local WARN_TAG = "[Rojo-Warn] "
|
||||||
|
|
||||||
local Log = {}
|
local Log = {}
|
||||||
|
|
||||||
Log.Level = Level
|
Log.Level = Level
|
||||||
|
|
||||||
|
function Log.setLogLevelThunk(thunk)
|
||||||
|
getLogLevel = thunk
|
||||||
|
end
|
||||||
|
|
||||||
function Log.trace(template, ...)
|
function Log.trace(template, ...)
|
||||||
if getLogLevel() >= Level.Trace then
|
if getLogLevel() >= Level.Trace then
|
||||||
print(addTags(TRACE_TAG, string.format(template, ...)))
|
print(addTags(TRACE_TAG, Fmt.fmt(template, ...)))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Log.info(template, ...)
|
function Log.info(template, ...)
|
||||||
if getLogLevel() >= Level.Info then
|
if getLogLevel() >= Level.Info then
|
||||||
print(addTags(INFO_TAG, string.format(template, ...)))
|
print(addTags(INFO_TAG, Fmt.fmt(template, ...)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Log.debug(template, ...)
|
||||||
|
if getLogLevel() >= Level.Debug then
|
||||||
|
print(addTags(DEBUG_TAG, Fmt.fmt(template, ...)))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Log.warn(template, ...)
|
function Log.warn(template, ...)
|
||||||
if getLogLevel() >= Level.Warning then
|
if getLogLevel() >= Level.Warning then
|
||||||
warn(addTags(WARN_TAG, string.format(template, ...)))
|
warn(addTags(WARN_TAG, Fmt.fmt(template, ...)))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
5
plugin/log/init.spec.lua
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
return function()
|
||||||
|
it("should load", function()
|
||||||
|
require(script.Parent)
|
||||||
|
end)
|
||||||
|
end
|
||||||
1
plugin/modules/rbx-dom
Submodule
1
plugin/modules/t
Submodule
@@ -1,51 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "rojo",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
|
|
||||||
"ReplicatedStorage": {
|
|
||||||
"$className": "ReplicatedStorage",
|
|
||||||
|
|
||||||
"Rojo": {
|
|
||||||
"$className": "Folder",
|
|
||||||
|
|
||||||
"Plugin": {
|
|
||||||
"$path": "src"
|
|
||||||
},
|
|
||||||
"Roact": {
|
|
||||||
"$path": "modules/roact/lib"
|
|
||||||
},
|
|
||||||
"Rodux": {
|
|
||||||
"$path": "modules/rodux/lib"
|
|
||||||
},
|
|
||||||
"RoactRodux": {
|
|
||||||
"$path": "modules/roact-rodux/lib"
|
|
||||||
},
|
|
||||||
"Promise": {
|
|
||||||
"$path": "modules/promise/lib"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"TestEZ": {
|
|
||||||
"$path": "modules/testez/lib"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"HttpService": {
|
|
||||||
"$className": "HttpService",
|
|
||||||
"$properties": {
|
|
||||||
"HttpEnabled": {
|
|
||||||
"Type": "Bool",
|
|
||||||
"Value": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"TestService": {
|
|
||||||
"$className": "TestService",
|
|
||||||
|
|
||||||
"TestBootstrap": {
|
|
||||||
"$path": "testBootstrap.server.lua"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "rojo",
|
|
||||||
"servePort": 8000,
|
|
||||||
"partitions": {
|
|
||||||
"plugin": {
|
|
||||||
"path": "src",
|
|
||||||
"target": "ReplicatedStorage.Rojo.Plugin"
|
|
||||||
},
|
|
||||||
"modules/roact": {
|
|
||||||
"path": "modules/roact/lib",
|
|
||||||
"target": "ReplicatedStorage.Rojo.Roact"
|
|
||||||
},
|
|
||||||
"modules/rodux": {
|
|
||||||
"path": "modules/rodux/lib",
|
|
||||||
"target": "ReplicatedStorage.Rojo.Rodux"
|
|
||||||
},
|
|
||||||
"modules/roact-rodux": {
|
|
||||||
"path": "modules/roact-rodux/lib",
|
|
||||||
"target": "ReplicatedStorage.Rojo.RoactRodux"
|
|
||||||
},
|
|
||||||
"modules/promise": {
|
|
||||||
"path": "modules/promise/lib",
|
|
||||||
"target": "ReplicatedStorage.Rojo.Promise"
|
|
||||||
},
|
|
||||||
"modules/testez": {
|
|
||||||
"path": "modules/testez/lib",
|
|
||||||
"target": "ReplicatedStorage.TestEZ"
|
|
||||||
},
|
|
||||||
"tests": {
|
|
||||||
"path": "testBootstrap.server.lua",
|
|
||||||
"target": "TestService.testBootstrap"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
local loadEnvironment = require("loadEnvironment")
|
|
||||||
|
|
||||||
local testPath = assert((...), "Please specify a path to a test file.")
|
|
||||||
|
|
||||||
local habitat = loadEnvironment()
|
|
||||||
|
|
||||||
local testModule = habitat:loadFromFs(testPath)
|
|
||||||
|
|
||||||
if testModule == nil then
|
|
||||||
error("Couldn't find test file at " .. testPath)
|
|
||||||
end
|
|
||||||
|
|
||||||
print("Starting test module.")
|
|
||||||
|
|
||||||
habitat:require(testModule)
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
--[[
|
|
||||||
Loads our library and all of its dependencies, then runs tests using TestEZ.
|
|
||||||
]]
|
|
||||||
|
|
||||||
local loadEnvironment = require("loadEnvironment")
|
|
||||||
|
|
||||||
local habitat, modules = loadEnvironment()
|
|
||||||
|
|
||||||
-- Load TestEZ and run our tests
|
|
||||||
local TestEZ = habitat:require(modules.TestEZ)
|
|
||||||
|
|
||||||
local results = TestEZ.TestBootstrap:run({modules.Rojo}, TestEZ.Reporters.TextReporter)
|
|
||||||
|
|
||||||
-- Did something go wrong?
|
|
||||||
if results.failureCount > 0 then
|
|
||||||
os.exit(1)
|
|
||||||
end
|
|
||||||
@@ -1,150 +1,216 @@
|
|||||||
|
local Http = require(script.Parent.Parent.Http)
|
||||||
|
local Log = require(script.Parent.Parent.Log)
|
||||||
local Promise = require(script.Parent.Parent.Promise)
|
local Promise = require(script.Parent.Parent.Promise)
|
||||||
|
|
||||||
local Config = require(script.Parent.Config)
|
local Config = require(script.Parent.Config)
|
||||||
|
local Types = require(script.Parent.Types)
|
||||||
local Version = require(script.Parent.Version)
|
local Version = require(script.Parent.Version)
|
||||||
local Http = require(script.Parent.Http)
|
|
||||||
local HttpError = require(script.Parent.HttpError)
|
|
||||||
|
|
||||||
local ApiContext = {}
|
local validateApiInfo = Types.ifEnabled(Types.ApiInfoResponse)
|
||||||
ApiContext.__index = ApiContext
|
local validateApiRead = Types.ifEnabled(Types.ApiReadResponse)
|
||||||
|
local validateApiSubscribe = Types.ifEnabled(Types.ApiSubscribeResponse)
|
||||||
|
|
||||||
-- TODO: Audit cases of errors and create enum values for each of them.
|
--[[
|
||||||
ApiContext.Error = {
|
Returns a promise that will never resolve nor reject.
|
||||||
ServerIdMismatch = "ServerIdMismatch",
|
]]
|
||||||
|
local function hangingPromise()
|
||||||
-- The server gave an unexpected 400-category error, which may be the
|
return Promise.new(function() end)
|
||||||
-- client's fault.
|
end
|
||||||
ClientError = "ClientError",
|
|
||||||
|
|
||||||
-- The server gave an unexpected 500-category error, which may be the
|
|
||||||
-- server's fault.
|
|
||||||
ServerError = "ServerError",
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(ApiContext.Error, {
|
|
||||||
__index = function(_, key)
|
|
||||||
error("Invalid ApiContext.Error name " .. key, 2)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
local function rejectFailedRequests(response)
|
local function rejectFailedRequests(response)
|
||||||
if response.code >= 400 then
|
if response.code >= 400 then
|
||||||
if response.code < 500 then
|
local message = string.format("HTTP %s:\n%s", tostring(response.code), response.body)
|
||||||
return Promise.reject(ApiContext.Error.ClientError)
|
|
||||||
else
|
return Promise.reject(message)
|
||||||
return Promise.reject(ApiContext.Error.ServerError)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return response
|
return response
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function rejectWrongProtocolVersion(infoResponseBody)
|
||||||
|
if infoResponseBody.protocolVersion ~= Config.protocolVersion then
|
||||||
|
local message = (
|
||||||
|
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
|
||||||
|
"\nMake sure you have matching versions of both the Rojo plugin and server!" ..
|
||||||
|
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
|
||||||
|
"\nYour server is version %s, with protocol version %s." ..
|
||||||
|
"\n\nGo to https://github.com/rojo-rbx/rojo for more details."
|
||||||
|
):format(
|
||||||
|
Version.display(Config.version), Config.protocolVersion,
|
||||||
|
Config.expectedServerVersionString,
|
||||||
|
infoResponseBody.serverVersion, infoResponseBody.protocolVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
return Promise.reject(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
return Promise.resolve(infoResponseBody)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function rejectWrongPlaceId(infoResponseBody)
|
||||||
|
if infoResponseBody.expectedPlaceIds ~= nil then
|
||||||
|
local foundId = false
|
||||||
|
|
||||||
|
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
|
||||||
|
if id == game.PlaceId then
|
||||||
|
foundId = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not foundId then
|
||||||
|
local idList = {}
|
||||||
|
for _, id in ipairs(infoResponseBody.expectedPlaceIds) do
|
||||||
|
table.insert(idList, "- " .. tostring(id))
|
||||||
|
end
|
||||||
|
|
||||||
|
local message = (
|
||||||
|
"Found a Rojo server, but its project is set to only be used with a specific list of places." ..
|
||||||
|
"\nYour place ID is %s, but needs to be one of these:" ..
|
||||||
|
"\n%s" ..
|
||||||
|
"\n\nTo change this list, edit 'servePlaceIds' in your .project.json file."
|
||||||
|
):format(
|
||||||
|
tostring(game.PlaceId),
|
||||||
|
table.concat(idList, "\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
return Promise.reject(message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Promise.resolve(infoResponseBody)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ApiContext = {}
|
||||||
|
ApiContext.__index = ApiContext
|
||||||
|
|
||||||
function ApiContext.new(baseUrl)
|
function ApiContext.new(baseUrl)
|
||||||
assert(type(baseUrl) == "string")
|
assert(type(baseUrl) == "string")
|
||||||
|
|
||||||
local self = {
|
local self = {
|
||||||
baseUrl = baseUrl,
|
__baseUrl = baseUrl,
|
||||||
serverId = nil,
|
__sessionId = nil,
|
||||||
rootInstanceId = nil,
|
__messageCursor = -1,
|
||||||
messageCursor = -1,
|
__connected = true,
|
||||||
partitionRoutes = nil,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setmetatable(self, ApiContext)
|
return setmetatable(self, ApiContext)
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:onMessage(callback)
|
function ApiContext:__fmtDebug(output)
|
||||||
self.onMessageCallback = callback
|
output:writeLine("ApiContext {{")
|
||||||
|
output:indent()
|
||||||
|
|
||||||
|
output:writeLine("Connected: {}", self.__connected)
|
||||||
|
output:writeLine("Base URL: {}", self.__baseUrl)
|
||||||
|
output:writeLine("Session ID: {}", self.__sessionId)
|
||||||
|
output:writeLine("Message Cursor: {}", self.__messageCursor)
|
||||||
|
|
||||||
|
output:unindent()
|
||||||
|
output:write("}")
|
||||||
|
end
|
||||||
|
|
||||||
|
function ApiContext:disconnect()
|
||||||
|
self.__connected = false
|
||||||
|
end
|
||||||
|
|
||||||
|
function ApiContext:setMessageCursor(index)
|
||||||
|
self.__messageCursor = index
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:connect()
|
function ApiContext:connect()
|
||||||
local url = ("%s/api/rojo"):format(self.baseUrl)
|
local url = ("%s/api/rojo"):format(self.__baseUrl)
|
||||||
|
|
||||||
return Http.get(url)
|
return Http.get(url)
|
||||||
:andThen(rejectFailedRequests)
|
:andThen(rejectFailedRequests)
|
||||||
:andThen(function(response)
|
:andThen(Http.Response.json)
|
||||||
local body = response:json()
|
:andThen(rejectWrongProtocolVersion)
|
||||||
|
:andThen(function(body)
|
||||||
|
assert(validateApiInfo(body))
|
||||||
|
|
||||||
if body.protocolVersion ~= Config.protocolVersion then
|
return body
|
||||||
local message = (
|
end)
|
||||||
"Found a Rojo dev server, but it's using a different protocol version, and is incompatible." ..
|
:andThen(rejectWrongPlaceId)
|
||||||
"\nMake sure you have matching versions of both the Rojo plugin and server!" ..
|
:andThen(function(body)
|
||||||
"\n\nYour client is version %s, with protocol version %s. It expects server version %s." ..
|
self.__sessionId = body.sessionId
|
||||||
"\nYour server is version %s, with protocol version %s." ..
|
|
||||||
"\n\nGo to https://github.com/LPGhatguy/rojo for more details."
|
|
||||||
):format(
|
|
||||||
Version.display(Config.version), Config.protocolVersion,
|
|
||||||
Config.expectedApiContextVersionString,
|
|
||||||
body.serverVersion, body.protocolVersion
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.reject(message)
|
return body
|
||||||
end
|
|
||||||
|
|
||||||
if body.expectedPlaceIds ~= nil then
|
|
||||||
local foundId = false
|
|
||||||
|
|
||||||
for _, id in ipairs(body.expectedPlaceIds) do
|
|
||||||
if id == game.PlaceId then
|
|
||||||
foundId = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not foundId then
|
|
||||||
local idList = {}
|
|
||||||
for _, id in ipairs(body.expectedPlaceIds) do
|
|
||||||
table.insert(idList, "- " .. tostring(id))
|
|
||||||
end
|
|
||||||
|
|
||||||
local message = (
|
|
||||||
"Found a Rojo server, but its project is set to only be used with a specific list of places." ..
|
|
||||||
"\nYour place ID is %s, but needs to be one of these:" ..
|
|
||||||
"\n%s" ..
|
|
||||||
"\n\nTo change this list, edit 'servePlaceIds' in roblox-project.json"
|
|
||||||
):format(
|
|
||||||
tostring(game.PlaceId),
|
|
||||||
table.concat(idList, "\n")
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.reject(message)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.serverId = body.serverId
|
|
||||||
self.partitionRoutes = body.partitions
|
|
||||||
self.rootInstanceId = body.rootInstanceId
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:read(ids)
|
function ApiContext:read(ids)
|
||||||
local url = ("%s/api/read/%s"):format(self.baseUrl, table.concat(ids, ","))
|
local url = ("%s/api/read/%s"):format(self.__baseUrl, table.concat(ids, ","))
|
||||||
|
|
||||||
return Http.get(url)
|
return Http.get(url)
|
||||||
:andThen(rejectFailedRequests)
|
:andThen(rejectFailedRequests)
|
||||||
:andThen(function(response)
|
:andThen(Http.Response.json)
|
||||||
local body = response:json()
|
:andThen(function(body)
|
||||||
|
if body.sessionId ~= self.__sessionId then
|
||||||
if body.serverId ~= self.serverId then
|
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
|
|
||||||
self.messageCursor = body.messageCursor
|
assert(validateApiRead(body))
|
||||||
|
|
||||||
|
return body
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ApiContext:write(patch)
|
||||||
|
local url = ("%s/api/write"):format(self.__baseUrl)
|
||||||
|
|
||||||
|
local updated = {}
|
||||||
|
for _, update in ipairs(patch.updated) do
|
||||||
|
local fixedUpdate = {
|
||||||
|
id = update.id,
|
||||||
|
changedName = update.changedName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if next(update.changedProperties) ~= nil then
|
||||||
|
fixedUpdate.changedProperties = update.changedProperties
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(updated, fixedUpdate)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Only add the 'added' field if the table is non-empty, or else Roblox's
|
||||||
|
-- JSON implementation will turn the table into an array instead of an
|
||||||
|
-- object, causing API validation to fail.
|
||||||
|
local added
|
||||||
|
if next(patch.added) ~= nil then
|
||||||
|
added = patch.added
|
||||||
|
end
|
||||||
|
|
||||||
|
local body = {
|
||||||
|
sessionId = self.__sessionId,
|
||||||
|
removed = patch.removed,
|
||||||
|
updated = updated,
|
||||||
|
added = added,
|
||||||
|
}
|
||||||
|
|
||||||
|
body = Http.jsonEncode(body)
|
||||||
|
|
||||||
|
return Http.post(url, body)
|
||||||
|
:andThen(rejectFailedRequests)
|
||||||
|
:andThen(Http.Response.json)
|
||||||
|
:andThen(function(body)
|
||||||
|
Log.info("Write response: {:?}", body)
|
||||||
|
|
||||||
return body
|
return body
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function ApiContext:retrieveMessages()
|
function ApiContext:retrieveMessages()
|
||||||
local url = ("%s/api/subscribe/%s"):format(self.baseUrl, self.messageCursor)
|
local url = ("%s/api/subscribe/%s"):format(self.__baseUrl, self.__messageCursor)
|
||||||
|
|
||||||
local function sendRequest()
|
local function sendRequest()
|
||||||
return Http.get(url)
|
return Http.get(url)
|
||||||
:catch(function(err)
|
:catch(function(err)
|
||||||
if err.type == HttpError.Error.Timeout then
|
if err.type == Http.Error.Kind.Timeout then
|
||||||
return sendRequest()
|
if self.__connected then
|
||||||
|
return sendRequest()
|
||||||
|
else
|
||||||
|
return hangingPromise()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return Promise.reject(err)
|
return Promise.reject(err)
|
||||||
@@ -153,14 +219,15 @@ function ApiContext:retrieveMessages()
|
|||||||
|
|
||||||
return sendRequest()
|
return sendRequest()
|
||||||
:andThen(rejectFailedRequests)
|
:andThen(rejectFailedRequests)
|
||||||
:andThen(function(response)
|
:andThen(Http.Response.json)
|
||||||
local body = response:json()
|
:andThen(function(body)
|
||||||
|
if body.sessionId ~= self.__sessionId then
|
||||||
if body.serverId ~= self.serverId then
|
|
||||||
return Promise.reject("Server changed ID")
|
return Promise.reject("Server changed ID")
|
||||||
end
|
end
|
||||||
|
|
||||||
self.messageCursor = body.messageCursor
|
assert(validateApiSubscribe(body))
|
||||||
|
|
||||||
|
self:setMessageCursor(body.messageCursor)
|
||||||
|
|
||||||
return body.messages
|
return body.messages
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
local sheetAsset = "rbxassetid://2738712459"
|
local strict = require(script.Parent.strict)
|
||||||
|
|
||||||
local Assets = {
|
local Assets = {
|
||||||
Sprites = {
|
Sprites = {},
|
||||||
WhiteCross = {
|
|
||||||
asset = sheetAsset,
|
|
||||||
offset = Vector2.new(190, 318),
|
|
||||||
size = Vector2.new(18, 18),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Slices = {
|
Slices = {
|
||||||
RoundBox = {
|
RoundBox = {
|
||||||
asset = "rbxassetid://2773204550",
|
asset = "rbxassetid://2773204550",
|
||||||
@@ -17,7 +11,8 @@ local Assets = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Images = {
|
Images = {
|
||||||
Logo = "rbxassetid://2773210620",
|
Logo = "rbxassetid://3405346157",
|
||||||
|
Icon = "rbxassetid://3405341609",
|
||||||
},
|
},
|
||||||
StartSession = "",
|
StartSession = "",
|
||||||
SessionActive = "",
|
SessionActive = "",
|
||||||
@@ -25,11 +20,7 @@ local Assets = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
local function guardForTypos(name, map)
|
local function guardForTypos(name, map)
|
||||||
setmetatable(map, {
|
strict(name, map)
|
||||||
__index = function(_, key)
|
|
||||||
error(("%q is not a valid member of %s"):format(tostring(key), name), 2)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
for key, child in pairs(map) do
|
for key, child in pairs(map) do
|
||||||
if type(child) == "table" then
|
if type(child) == "table" then
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
|
||||||
local Plugin = script:FindFirstAncestor("Plugin")
|
local Roact = require(Rojo.Roact)
|
||||||
|
local Log = require(Rojo.Log)
|
||||||
|
|
||||||
|
local ApiContext = require(Plugin.ApiContext)
|
||||||
local Assets = require(Plugin.Assets)
|
local Assets = require(Plugin.Assets)
|
||||||
local Session = require(Plugin.Session)
|
|
||||||
local Config = require(Plugin.Config)
|
local Config = require(Plugin.Config)
|
||||||
local Version = require(Plugin.Version)
|
|
||||||
local Logging = require(Plugin.Logging)
|
|
||||||
local DevSettings = require(Plugin.DevSettings)
|
local DevSettings = require(Plugin.DevSettings)
|
||||||
|
local ServeSession = require(Plugin.ServeSession)
|
||||||
|
local Version = require(Plugin.Version)
|
||||||
|
local preloadAssets = require(Plugin.preloadAssets)
|
||||||
|
local strict = require(Plugin.strict)
|
||||||
|
|
||||||
local ConnectPanel = require(Plugin.Components.ConnectPanel)
|
local ConnectPanel = require(Plugin.Components.ConnectPanel)
|
||||||
|
local ConnectingPanel = require(Plugin.Components.ConnectingPanel)
|
||||||
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
|
local ConnectionActivePanel = require(Plugin.Components.ConnectionActivePanel)
|
||||||
|
local ErrorPanel = require(Plugin.Components.ErrorPanel)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
@@ -24,7 +30,7 @@ local function showUpgradeMessage(lastVersion)
|
|||||||
Version.display(Config.version), Config.expectedServerVersionString
|
Version.display(Config.version), Config.expectedServerVersionString
|
||||||
)
|
)
|
||||||
|
|
||||||
Logging.info(message)
|
Log.info(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
@@ -50,145 +56,169 @@ local function checkUpgrade(plugin)
|
|||||||
plugin:SetSetting("LastRojoVersion", Config.version)
|
plugin:SetSetting("LastRojoVersion", Config.version)
|
||||||
end
|
end
|
||||||
|
|
||||||
local SessionStatus = {
|
local AppStatus = strict("AppStatus", {
|
||||||
Disconnected = "Disconnected",
|
NotStarted = "NotStarted",
|
||||||
|
Connecting = "Connecting",
|
||||||
Connected = "Connected",
|
Connected = "Connected",
|
||||||
ConfiguringSession = "ConfiguringSession",
|
Error = "Error",
|
||||||
-- TODO: Error?
|
|
||||||
}
|
|
||||||
|
|
||||||
setmetatable(SessionStatus, {
|
|
||||||
__index = function(_, key)
|
|
||||||
error(("%q is not a valid member of SessionStatus"):format(tostring(key)), 2)
|
|
||||||
end,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
local App = Roact.Component:extend("App")
|
local App = Roact.Component:extend("App")
|
||||||
|
|
||||||
function App:init()
|
function App:init()
|
||||||
self:setState({
|
self:setState({
|
||||||
sessionStatus = SessionStatus.Disconnected,
|
appStatus = AppStatus.NotStarted,
|
||||||
|
errorMessage = nil,
|
||||||
})
|
})
|
||||||
|
|
||||||
self.connectButton = nil
|
self.signals = {}
|
||||||
self.currentSession = nil
|
self.serveSession = nil
|
||||||
|
|
||||||
self.displayedVersion = DevSettings:isEnabled()
|
self.displayedVersion = DevSettings:isEnabled()
|
||||||
and Config.codename
|
and Config.codename
|
||||||
or Version.display(Config.version)
|
or Version.display(Config.version)
|
||||||
|
|
||||||
|
local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion)
|
||||||
|
|
||||||
|
self.toggleButton = toolbar:CreateButton(
|
||||||
|
"Rojo",
|
||||||
|
"Show or hide the Rojo panel",
|
||||||
|
Assets.Images.Icon)
|
||||||
|
self.toggleButton.ClickableWhenViewportHidden = true
|
||||||
|
self.toggleButton.Click:Connect(function()
|
||||||
|
self.dockWidget.Enabled = not self.dockWidget.Enabled
|
||||||
|
end)
|
||||||
|
|
||||||
|
local widgetInfo = DockWidgetPluginGuiInfo.new(
|
||||||
|
Enum.InitialDockState.Right,
|
||||||
|
false, -- Initially enabled state
|
||||||
|
false, -- Whether to override the widget's previous state
|
||||||
|
360, 190, -- Floating size
|
||||||
|
360, 190 -- Minimum size
|
||||||
|
)
|
||||||
|
|
||||||
|
self.dockWidget = self.props.plugin:CreateDockWidgetPluginGui("Rojo-" .. self.displayedVersion, widgetInfo)
|
||||||
|
self.dockWidget.Name = "Rojo " .. self.displayedVersion
|
||||||
|
self.dockWidget.Title = "Rojo " .. self.displayedVersion
|
||||||
|
self.dockWidget.AutoLocalize = false
|
||||||
|
self.dockWidget.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
|
||||||
|
|
||||||
|
self.signals.dockWidgetEnabled = self.dockWidget:GetPropertyChangedSignal("Enabled"):Connect(function()
|
||||||
|
self.toggleButton:SetActive(self.dockWidget.Enabled)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function App:startSession(address, port)
|
||||||
|
Log.trace("Starting new session")
|
||||||
|
|
||||||
|
local baseUrl = ("http://%s:%s"):format(address, port)
|
||||||
|
self.serveSession = ServeSession.new({
|
||||||
|
apiContext = ApiContext.new(baseUrl),
|
||||||
|
})
|
||||||
|
|
||||||
|
self.serveSession:onStatusChanged(function(status, details)
|
||||||
|
if status == ServeSession.Status.Connecting then
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.Connecting,
|
||||||
|
})
|
||||||
|
elseif status == ServeSession.Status.Connected then
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.Connected,
|
||||||
|
})
|
||||||
|
elseif status == ServeSession.Status.Disconnected then
|
||||||
|
self.serveSession = nil
|
||||||
|
|
||||||
|
-- Details being present indicates that this
|
||||||
|
-- disconnection was from an error.
|
||||||
|
if details ~= nil then
|
||||||
|
Log.warn("Disconnected from an error: {}", details)
|
||||||
|
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.Error,
|
||||||
|
errorMessage = tostring(details),
|
||||||
|
})
|
||||||
|
else
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.NotStarted,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
self.serveSession:start()
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:render()
|
function App:render()
|
||||||
local children
|
local children
|
||||||
|
|
||||||
if self.state.sessionStatus == SessionStatus.Connected then
|
if self.state.appStatus == AppStatus.NotStarted then
|
||||||
children = {
|
|
||||||
ConnectionActivePanel = e(ConnectionActivePanel, {
|
|
||||||
stopSession = function()
|
|
||||||
Logging.trace("Disconnecting session")
|
|
||||||
|
|
||||||
self.currentSession:disconnect()
|
|
||||||
self.currentSession = nil
|
|
||||||
self:setState({
|
|
||||||
sessionStatus = SessionStatus.Disconnected,
|
|
||||||
})
|
|
||||||
|
|
||||||
Logging.trace("Session terminated by user")
|
|
||||||
end,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
elseif self.state.sessionStatus == SessionStatus.ConfiguringSession then
|
|
||||||
children = {
|
children = {
|
||||||
ConnectPanel = e(ConnectPanel, {
|
ConnectPanel = e(ConnectPanel, {
|
||||||
startSession = function(address, port)
|
startSession = function(address, port)
|
||||||
Logging.trace("Starting new session")
|
self:startSession(address, port)
|
||||||
|
|
||||||
local success, session = Session.new({
|
|
||||||
address = address,
|
|
||||||
port = port,
|
|
||||||
onError = function(message)
|
|
||||||
Logging.warn("Rojo session terminated because of an error:\n%s", tostring(message))
|
|
||||||
self.currentSession = nil
|
|
||||||
|
|
||||||
self:setState({
|
|
||||||
sessionStatus = SessionStatus.Disconnected,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
if success then
|
|
||||||
self.currentSession = session
|
|
||||||
self:setState({
|
|
||||||
sessionStatus = SessionStatus.Connected,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end,
|
end,
|
||||||
cancel = function()
|
cancel = function()
|
||||||
Logging.trace("Canceling session configuration")
|
Log.trace("Canceling session configuration")
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
sessionStatus = SessionStatus.Disconnected,
|
appStatus = AppStatus.NotStarted,
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
elseif self.state.appStatus == AppStatus.Connecting then
|
||||||
|
children = {
|
||||||
|
ConnectingPanel = Roact.createElement(ConnectingPanel),
|
||||||
|
}
|
||||||
|
elseif self.state.appStatus == AppStatus.Connected then
|
||||||
|
children = {
|
||||||
|
ConnectionActivePanel = e(ConnectionActivePanel, {
|
||||||
|
stopSession = function()
|
||||||
|
Log.trace("Disconnecting session")
|
||||||
|
|
||||||
|
self.serveSession:stop()
|
||||||
|
self.serveSession = nil
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.NotStarted,
|
||||||
|
})
|
||||||
|
|
||||||
|
Log.trace("Session terminated by user")
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
elseif self.state.appStatus == AppStatus.Error then
|
||||||
|
children = {
|
||||||
|
ErrorPanel = Roact.createElement(ErrorPanel, {
|
||||||
|
errorMessage = self.state.errorMessage,
|
||||||
|
onDismiss = function()
|
||||||
|
self:setState({
|
||||||
|
appStatus = AppStatus.NotStarted,
|
||||||
})
|
})
|
||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return e("ScreenGui", {
|
return Roact.createElement(Roact.Portal, {
|
||||||
AutoLocalize = false,
|
target = self.dockWidget,
|
||||||
ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
|
|
||||||
}, children)
|
}, children)
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:didMount()
|
function App:didMount()
|
||||||
Logging.trace("Rojo %s initializing", self.displayedVersion)
|
Log.trace("Rojo {} initializing", self.displayedVersion)
|
||||||
|
|
||||||
local toolbar = self.props.plugin:CreateToolbar("Rojo " .. self.displayedVersion)
|
checkUpgrade(self.props.plugin)
|
||||||
|
preloadAssets()
|
||||||
self.connectButton = toolbar:CreateButton(
|
|
||||||
"Connect",
|
|
||||||
"Connect to a running Rojo session",
|
|
||||||
Assets.StartSession)
|
|
||||||
self.connectButton.ClickableWhenViewportHidden = false
|
|
||||||
self.connectButton.Click:Connect(function()
|
|
||||||
checkUpgrade(self.props.plugin)
|
|
||||||
|
|
||||||
if self.state.sessionStatus == SessionStatus.Connected then
|
|
||||||
Logging.trace("Disconnecting session")
|
|
||||||
|
|
||||||
self.currentSession:disconnect()
|
|
||||||
self.currentSession = nil
|
|
||||||
self:setState({
|
|
||||||
sessionStatus = SessionStatus.Disconnected,
|
|
||||||
})
|
|
||||||
|
|
||||||
Logging.trace("Session terminated by user")
|
|
||||||
elseif self.state.sessionStatus == SessionStatus.Disconnected then
|
|
||||||
Logging.trace("Starting session configuration")
|
|
||||||
|
|
||||||
self:setState({
|
|
||||||
sessionStatus = SessionStatus.ConfiguringSession,
|
|
||||||
})
|
|
||||||
elseif self.state.sessionStatus == SessionStatus.ConfiguringSession then
|
|
||||||
Logging.trace("Canceling session configuration")
|
|
||||||
|
|
||||||
self:setState({
|
|
||||||
sessionStatus = SessionStatus.Disconnected,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function App:didUpdate()
|
function App:willUnmount()
|
||||||
local connectActive = self.state.sessionStatus == SessionStatus.ConfiguringSession
|
if self.serveSession ~= nil then
|
||||||
or self.state.sessionStatus == SessionStatus.Connected
|
self.serveSession:stop()
|
||||||
|
self.serveSession = nil
|
||||||
|
end
|
||||||
|
|
||||||
self.connectButton:SetActive(connectActive)
|
for _, signal in pairs(self.signals) do
|
||||||
|
signal:Disconnect()
|
||||||
if self.state.sessionStatus == SessionStatus.Connected then
|
|
||||||
self.connectButton.Icon = Assets.SessionActive
|
|
||||||
else
|
|
||||||
self.connectButton.Icon = Assets.StartSession
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -4,39 +4,19 @@ local Plugin = Rojo.Plugin
|
|||||||
local Roact = require(Rojo.Roact)
|
local Roact = require(Rojo.Roact)
|
||||||
|
|
||||||
local Config = require(Plugin.Config)
|
local Config = require(Plugin.Config)
|
||||||
local Version = require(Plugin.Version)
|
|
||||||
local Assets = require(Plugin.Assets)
|
|
||||||
local Theme = require(Plugin.Theme)
|
local Theme = require(Plugin.Theme)
|
||||||
local joinBindings = require(Plugin.joinBindings)
|
|
||||||
|
|
||||||
|
local Panel = require(Plugin.Components.Panel)
|
||||||
local FitList = require(Plugin.Components.FitList)
|
local FitList = require(Plugin.Components.FitList)
|
||||||
local FitText = require(Plugin.Components.FitText)
|
local FitText = require(Plugin.Components.FitText)
|
||||||
local FormButton = require(Plugin.Components.FormButton)
|
local FormButton = require(Plugin.Components.FormButton)
|
||||||
local FormTextInput = require(Plugin.Components.FormTextInput)
|
local FormTextInput = require(Plugin.Components.FormTextInput)
|
||||||
|
|
||||||
local RoundBox = Assets.Slices.RoundBox
|
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local ConnectPanel = Roact.Component:extend("ConnectPanel")
|
local ConnectPanel = Roact.Component:extend("ConnectPanel")
|
||||||
|
|
||||||
function ConnectPanel:init()
|
function ConnectPanel:init()
|
||||||
self.footerSize, self.setFooterSize = Roact.createBinding(Vector2.new())
|
|
||||||
self.footerVersionSize, self.setFooterVersionSize = Roact.createBinding(Vector2.new())
|
|
||||||
|
|
||||||
-- This is constructed in init because 'joinBindings' is a hack and we'd
|
|
||||||
-- leak memory constructing it every render. When this kind of feature lands
|
|
||||||
-- in Roact properly, we can do this inline in render without fear.
|
|
||||||
self.footerRestSize = joinBindings(
|
|
||||||
{
|
|
||||||
self.footerSize,
|
|
||||||
self.footerVersionSize,
|
|
||||||
},
|
|
||||||
function(container, other)
|
|
||||||
return UDim2.new(0, container.X - other.X - 16, 0, 32)
|
|
||||||
end
|
|
||||||
)
|
|
||||||
|
|
||||||
self:setState({
|
self:setState({
|
||||||
address = "",
|
address = "",
|
||||||
port = "",
|
port = "",
|
||||||
@@ -45,24 +25,14 @@ end
|
|||||||
|
|
||||||
function ConnectPanel:render()
|
function ConnectPanel:render()
|
||||||
local startSession = self.props.startSession
|
local startSession = self.props.startSession
|
||||||
local cancel = self.props.cancel
|
|
||||||
|
|
||||||
return e(FitList, {
|
return e(Panel, nil, {
|
||||||
containerKind = "ImageLabel",
|
Layout = e("UIListLayout", {
|
||||||
containerProps = {
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
Image = RoundBox.asset,
|
|
||||||
ImageRectOffset = RoundBox.offset,
|
|
||||||
ImageRectSize = RoundBox.size,
|
|
||||||
SliceCenter = RoundBox.center,
|
|
||||||
ScaleType = Enum.ScaleType.Slice,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
|
||||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
|
||||||
},
|
|
||||||
layoutProps = {
|
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
},
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
}, {
|
}),
|
||||||
|
|
||||||
Inputs = e(FitList, {
|
Inputs = e(FitList, {
|
||||||
containerProps = {
|
containerProps = {
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
@@ -96,7 +66,7 @@ function ConnectPanel:render()
|
|||||||
Font = Theme.TitleFont,
|
Font = Theme.TitleFont,
|
||||||
TextSize = 20,
|
TextSize = 20,
|
||||||
Text = "Address",
|
Text = "Address",
|
||||||
TextColor3 = Theme.AccentColor,
|
TextColor3 = Theme.PrimaryColor,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Input = e(FormTextInput, {
|
Input = e(FormTextInput, {
|
||||||
@@ -129,7 +99,7 @@ function ConnectPanel:render()
|
|||||||
Font = Theme.TitleFont,
|
Font = Theme.TitleFont,
|
||||||
TextSize = 20,
|
TextSize = 20,
|
||||||
Text = "Port",
|
Text = "Port",
|
||||||
TextColor3 = Theme.AccentColor,
|
TextColor3 = Theme.PrimaryColor,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Input = e(FormTextInput, {
|
Input = e(FormTextInput, {
|
||||||
@@ -165,17 +135,6 @@ function ConnectPanel:render()
|
|||||||
PaddingRight = UDim.new(0, 24),
|
PaddingRight = UDim.new(0, 24),
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
e(FormButton, {
|
|
||||||
layoutOrder = 1,
|
|
||||||
text = "Cancel",
|
|
||||||
onClick = function()
|
|
||||||
if cancel ~= nil then
|
|
||||||
cancel()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
secondary = true,
|
|
||||||
}),
|
|
||||||
|
|
||||||
e(FormButton, {
|
e(FormButton, {
|
||||||
layoutOrder = 2,
|
layoutOrder = 2,
|
||||||
text = "Connect",
|
text = "Connect",
|
||||||
@@ -196,65 +155,6 @@ function ConnectPanel:render()
|
|||||||
end,
|
end,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
Footer = e(FitList, {
|
|
||||||
fitAxes = "Y",
|
|
||||||
containerKind = "ImageLabel",
|
|
||||||
containerProps = {
|
|
||||||
Image = RoundBox.asset,
|
|
||||||
ImageRectOffset = RoundBox.offset + Vector2.new(0, RoundBox.size.Y / 2),
|
|
||||||
ImageRectSize = RoundBox.size * Vector2.new(1, 0.5),
|
|
||||||
SliceCenter = RoundBox.center,
|
|
||||||
ScaleType = Enum.ScaleType.Slice,
|
|
||||||
ImageColor3 = Theme.SecondaryColor,
|
|
||||||
Size = UDim2.new(1, 0, 0, 0),
|
|
||||||
LayoutOrder = 3,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
|
|
||||||
[Roact.Change.AbsoluteSize] = function(rbx)
|
|
||||||
self.setFooterSize(rbx.AbsoluteSize)
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
layoutProps = {
|
|
||||||
FillDirection = Enum.FillDirection.Horizontal,
|
|
||||||
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
|
||||||
},
|
|
||||||
paddingProps = {
|
|
||||||
PaddingTop = UDim.new(0, 4),
|
|
||||||
PaddingBottom = UDim.new(0, 4),
|
|
||||||
PaddingLeft = UDim.new(0, 8),
|
|
||||||
PaddingRight = UDim.new(0, 8),
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
LogoContainer = e("Frame", {
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
|
|
||||||
Size = self.footerRestSize,
|
|
||||||
}, {
|
|
||||||
Logo = e("ImageLabel", {
|
|
||||||
Image = Assets.Images.Logo,
|
|
||||||
Size = UDim2.new(0, 80, 0, 40),
|
|
||||||
ScaleType = Enum.ScaleType.Fit,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
Position = UDim2.new(0, 0, 1, -10),
|
|
||||||
AnchorPoint = Vector2.new(0, 1),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
|
|
||||||
Version = e(FitText, {
|
|
||||||
Font = Theme.TitleFont,
|
|
||||||
TextSize = 18,
|
|
||||||
Text = Version.display(Config.version),
|
|
||||||
TextXAlignment = Enum.TextXAlignment.Right,
|
|
||||||
TextColor3 = Theme.LightTextColor,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
|
|
||||||
[Roact.Change.AbsoluteSize] = function(rbx)
|
|
||||||
self.setFooterVersionSize(rbx.AbsoluteSize)
|
|
||||||
end,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
34
plugin/src/Components/ConnectingPanel.lua
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||||
|
|
||||||
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
|
local Theme = require(Plugin.Theme)
|
||||||
|
|
||||||
|
local Panel = require(Plugin.Components.Panel)
|
||||||
|
local FitText = require(Plugin.Components.FitText)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local ConnectingPanel = Roact.Component:extend("ConnectingPanel")
|
||||||
|
|
||||||
|
function ConnectingPanel:render()
|
||||||
|
return e(Panel, nil, {
|
||||||
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 8),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Text = e(FitText, {
|
||||||
|
Padding = Vector2.new(12, 6),
|
||||||
|
Font = Theme.ButtonFont,
|
||||||
|
TextSize = 18,
|
||||||
|
Text = "Connecting...",
|
||||||
|
TextColor3 = Theme.PrimaryColor,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return ConnectingPanel
|
||||||
@@ -3,63 +3,42 @@ local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
|||||||
local Plugin = script:FindFirstAncestor("Plugin")
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
local Theme = require(Plugin.Theme)
|
local Theme = require(Plugin.Theme)
|
||||||
local Assets = require(Plugin.Assets)
|
|
||||||
|
|
||||||
local FitList = require(Plugin.Components.FitList)
|
local Panel = require(Plugin.Components.Panel)
|
||||||
local FitText = require(Plugin.Components.FitText)
|
local FitText = require(Plugin.Components.FitText)
|
||||||
|
local FormButton = require(Plugin.Components.FormButton)
|
||||||
|
|
||||||
local e = Roact.createElement
|
local e = Roact.createElement
|
||||||
|
|
||||||
local RoundBox = Assets.Slices.RoundBox
|
local ConnectionActivePanel = Roact.Component:extend("ConnectionActivePanel")
|
||||||
local WhiteCross = Assets.Sprites.WhiteCross
|
|
||||||
|
|
||||||
local function ConnectionActivePanel(props)
|
function ConnectionActivePanel:render()
|
||||||
local stopSession = props.stopSession
|
local stopSession = self.props.stopSession
|
||||||
|
|
||||||
return e(FitList, {
|
return e(Panel, nil, {
|
||||||
containerKind = "ImageLabel",
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
containerProps = {
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
Image = RoundBox.asset,
|
|
||||||
ImageRectOffset = RoundBox.offset + Vector2.new(0, RoundBox.size.Y / 2),
|
|
||||||
ImageRectSize = RoundBox.size * Vector2.new(1, 0.5),
|
|
||||||
SliceCenter = Rect.new(4, 4, 4, 4),
|
|
||||||
ScaleType = Enum.ScaleType.Slice,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
Position = UDim2.new(0.5, 0, 0, 0),
|
|
||||||
AnchorPoint = Vector2.new(0.5, 0),
|
|
||||||
},
|
|
||||||
layoutProps = {
|
|
||||||
FillDirection = Enum.FillDirection.Horizontal,
|
|
||||||
VerticalAlignment = Enum.VerticalAlignment.Center,
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
},
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
}, {
|
Padding = UDim.new(0, 8),
|
||||||
|
}),
|
||||||
|
|
||||||
Text = e(FitText, {
|
Text = e(FitText, {
|
||||||
Padding = Vector2.new(12, 6),
|
Padding = Vector2.new(12, 6),
|
||||||
Font = Theme.ButtonFont,
|
Font = Theme.ButtonFont,
|
||||||
TextSize = 18,
|
TextSize = 18,
|
||||||
Text = "Rojo Connected",
|
Text = "Connected to Live-Sync Server",
|
||||||
TextColor3 = Theme.PrimaryColor,
|
TextColor3 = Theme.PrimaryColor,
|
||||||
BackgroundTransparency = 1,
|
BackgroundTransparency = 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
CloseContainer = e("ImageButton", {
|
DisconnectButton = e(FormButton, {
|
||||||
Size = UDim2.new(0, 30, 0, 30),
|
layoutOrder = 2,
|
||||||
BackgroundTransparency = 1,
|
text = "Disconnect",
|
||||||
|
secondary = true,
|
||||||
[Roact.Event.Activated] = function()
|
onClick = function()
|
||||||
stopSession()
|
stopSession()
|
||||||
end,
|
end,
|
||||||
}, {
|
|
||||||
CloseImage = e("ImageLabel", {
|
|
||||||
Size = UDim2.new(0, 16, 0, 16),
|
|
||||||
Position = UDim2.new(0.5, 0, 0.5, 0),
|
|
||||||
AnchorPoint = Vector2.new(0.5, 0.5),
|
|
||||||
Image = WhiteCross.asset,
|
|
||||||
ImageRectOffset = WhiteCross.offset,
|
|
||||||
ImageRectSize = WhiteCross.size,
|
|
||||||
ImageColor3 = Theme.PrimaryColor,
|
|
||||||
BackgroundTransparency = 1,
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|||||||
69
plugin/src/Components/ErrorPanel.lua
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||||
|
|
||||||
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
|
local Theme = require(Plugin.Theme)
|
||||||
|
|
||||||
|
local Panel = require(Plugin.Components.Panel)
|
||||||
|
local FitText = require(Plugin.Components.FitText)
|
||||||
|
local FitScrollingFrame = require(Plugin.Components.FitScrollingFrame)
|
||||||
|
local FormButton = require(Plugin.Components.FormButton)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local BUTTON_HEIGHT = 60
|
||||||
|
local HOR_PADDING = 8
|
||||||
|
|
||||||
|
local ErrorPanel = Roact.Component:extend("ErrorPanel")
|
||||||
|
|
||||||
|
function ErrorPanel:render()
|
||||||
|
local errorMessage = self.props.errorMessage
|
||||||
|
local onDismiss = self.props.onDismiss
|
||||||
|
|
||||||
|
return e(Panel, nil, {
|
||||||
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
Padding = UDim.new(0, 8),
|
||||||
|
}),
|
||||||
|
|
||||||
|
ErrorContainer = e(FitScrollingFrame, {
|
||||||
|
containerProps = {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
Size = UDim2.new(1, -HOR_PADDING * 2, 1, -BUTTON_HEIGHT),
|
||||||
|
Position = UDim2.new(0, HOR_PADDING, 0, 0),
|
||||||
|
ScrollBarImageColor3 = Theme.PrimaryColor,
|
||||||
|
VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar,
|
||||||
|
ScrollingDirection = Enum.ScrollingDirection.Y,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Text = e(FitText, {
|
||||||
|
Size = UDim2.new(1, 0, 0, 0),
|
||||||
|
|
||||||
|
LayoutOrder = 1,
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Left,
|
||||||
|
TextYAlignment = Enum.TextYAlignment.Top,
|
||||||
|
FitAxis = "Y",
|
||||||
|
Font = Theme.ButtonFont,
|
||||||
|
TextSize = 18,
|
||||||
|
Text = errorMessage,
|
||||||
|
TextWrap = true,
|
||||||
|
TextColor3 = Theme.PrimaryColor,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
DismissButton = e(FormButton, {
|
||||||
|
layoutOrder = 2,
|
||||||
|
text = "Dismiss",
|
||||||
|
secondary = true,
|
||||||
|
onClick = function()
|
||||||
|
onDismiss()
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return ErrorPanel
|
||||||
33
plugin/src/Components/FitScrollingFrame.lua
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||||
|
|
||||||
|
local Dictionary = require(script.Parent.Parent.Dictionary)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local FitScrollingFrame = Roact.Component:extend("FitScrollingFrame")
|
||||||
|
|
||||||
|
function FitScrollingFrame:init()
|
||||||
|
self.sizeBinding, self.setSize = Roact.createBinding(UDim2.new())
|
||||||
|
end
|
||||||
|
|
||||||
|
function FitScrollingFrame:render()
|
||||||
|
local containerProps = self.props.containerProps
|
||||||
|
local layoutProps = self.props.layoutProps
|
||||||
|
|
||||||
|
local children = Dictionary.merge(self.props[Roact.Children], {
|
||||||
|
["$Layout"] = e("UIListLayout", Dictionary.merge({
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
[Roact.Change.AbsoluteContentSize] = function(instance)
|
||||||
|
self.setSize(UDim2.new(0, 0, 0, instance.AbsoluteContentSize.Y))
|
||||||
|
end,
|
||||||
|
}, layoutProps)),
|
||||||
|
})
|
||||||
|
|
||||||
|
local fullContainerProps = Dictionary.merge(containerProps, {
|
||||||
|
CanvasSize = self.sizeBinding,
|
||||||
|
})
|
||||||
|
|
||||||
|
return e("ScrollingFrame", fullContainerProps, children)
|
||||||
|
end
|
||||||
|
|
||||||
|
return FitScrollingFrame
|
||||||
@@ -9,6 +9,7 @@ local e = Roact.createElement
|
|||||||
local FitText = Roact.Component:extend("FitText")
|
local FitText = Roact.Component:extend("FitText")
|
||||||
|
|
||||||
function FitText:init()
|
function FitText:init()
|
||||||
|
self.ref = Roact.createRef()
|
||||||
self.sizeBinding, self.setSize = Roact.createBinding(UDim2.new())
|
self.sizeBinding, self.setSize = Roact.createBinding(UDim2.new())
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -16,10 +17,15 @@ function FitText:render()
|
|||||||
local kind = self.props.Kind or "TextLabel"
|
local kind = self.props.Kind or "TextLabel"
|
||||||
|
|
||||||
local containerProps = Dictionary.merge(self.props, {
|
local containerProps = Dictionary.merge(self.props, {
|
||||||
|
FitAxis = Dictionary.None,
|
||||||
Kind = Dictionary.None,
|
Kind = Dictionary.None,
|
||||||
Padding = Dictionary.None,
|
Padding = Dictionary.None,
|
||||||
MinSize = Dictionary.None,
|
MinSize = Dictionary.None,
|
||||||
Size = self.sizeBinding
|
Size = self.sizeBinding,
|
||||||
|
[Roact.Ref] = self.ref,
|
||||||
|
[Roact.Change.AbsoluteSize] = function()
|
||||||
|
self:updateTextMeasurements()
|
||||||
|
end
|
||||||
})
|
})
|
||||||
|
|
||||||
return e(kind, containerProps)
|
return e(kind, containerProps)
|
||||||
@@ -36,15 +42,45 @@ end
|
|||||||
function FitText:updateTextMeasurements()
|
function FitText:updateTextMeasurements()
|
||||||
local minSize = self.props.MinSize or Vector2.new(0, 0)
|
local minSize = self.props.MinSize or Vector2.new(0, 0)
|
||||||
local padding = self.props.Padding or Vector2.new(0, 0)
|
local padding = self.props.Padding or Vector2.new(0, 0)
|
||||||
|
local fitAxis = self.props.FitAxis or "XY"
|
||||||
|
local baseSize = self.props.Size
|
||||||
|
|
||||||
local text = self.props.Text or ""
|
local text = self.props.Text or ""
|
||||||
local font = self.props.Font or Enum.Font.Legacy
|
local font = self.props.Font or Enum.Font.Legacy
|
||||||
local textSize = self.props.TextSize or 12
|
local textSize = self.props.TextSize or 12
|
||||||
|
|
||||||
local measuredText = TextService:GetTextSize(text, textSize, font, Vector2.new(9e6, 9e6))
|
local containerSize = self.ref.current.AbsoluteSize
|
||||||
local totalSize = UDim2.new(
|
|
||||||
0, math.max(minSize.X, padding.X * 2 + measuredText.X),
|
local textBounds
|
||||||
0, math.max(minSize.Y, padding.Y * 2 + measuredText.Y))
|
|
||||||
|
if fitAxis == "XY" then
|
||||||
|
textBounds = Vector2.new(9e6, 9e6)
|
||||||
|
elseif fitAxis == "X" then
|
||||||
|
textBounds = Vector2.new(9e6, containerSize.Y - padding.Y * 2)
|
||||||
|
elseif fitAxis == "Y" then
|
||||||
|
textBounds = Vector2.new(containerSize.X - padding.X * 2, 9e6)
|
||||||
|
end
|
||||||
|
|
||||||
|
local measuredText = TextService:GetTextSize(text, textSize, font, textBounds)
|
||||||
|
|
||||||
|
local computedX = math.max(minSize.X, padding.X * 2 + measuredText.X)
|
||||||
|
local computedY = math.max(minSize.Y, padding.Y * 2 + measuredText.Y)
|
||||||
|
|
||||||
|
local totalSize
|
||||||
|
|
||||||
|
if fitAxis == "XY" then
|
||||||
|
totalSize = UDim2.new(
|
||||||
|
0, computedX,
|
||||||
|
0, computedY)
|
||||||
|
elseif fitAxis == "X" then
|
||||||
|
totalSize = UDim2.new(
|
||||||
|
0, computedX,
|
||||||
|
baseSize.Y.Scale, baseSize.Y.Offset)
|
||||||
|
elseif fitAxis == "Y" then
|
||||||
|
totalSize = UDim2.new(
|
||||||
|
baseSize.X.Scale, baseSize.X.Offset,
|
||||||
|
0, computedY)
|
||||||
|
end
|
||||||
|
|
||||||
self.setSize(totalSize)
|
self.setSize(totalSize)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ function FormTextInput:render()
|
|||||||
TextSize = TEXT_SIZE,
|
TextSize = TEXT_SIZE,
|
||||||
Text = value,
|
Text = value,
|
||||||
PlaceholderText = shownPlaceholder,
|
PlaceholderText = shownPlaceholder,
|
||||||
PlaceholderColor3 = Theme.AccentLightColor,
|
PlaceholderColor3 = Theme.LightTextColor,
|
||||||
TextColor3 = Theme.AccentColor,
|
TextColor3 = Theme.PrimaryColor,
|
||||||
|
|
||||||
[Roact.Change.Text] = function(rbx)
|
[Roact.Change.Text] = function(rbx)
|
||||||
onValueChange(rbx.Text)
|
onValueChange(rbx.Text)
|
||||||
|
|||||||
34
plugin/src/Components/Panel.lua
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
local Roact = require(script:FindFirstAncestor("Rojo").Roact)
|
||||||
|
|
||||||
|
local Plugin = script:FindFirstAncestor("Plugin")
|
||||||
|
|
||||||
|
local RojoFooter = require(Plugin.Components.RojoFooter)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local Panel = Roact.Component:extend("Panel")
|
||||||
|
|
||||||
|
function Panel:init()
|
||||||
|
self.footerSize, self.setFooterSize = Roact.createBinding(Vector2.new())
|
||||||
|
end
|
||||||
|
|
||||||
|
function Panel:render()
|
||||||
|
return e("Frame", {
|
||||||
|
Size = UDim2.new(1, 0, 1, 0),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, {
|
||||||
|
Layout = Roact.createElement("UIListLayout", {
|
||||||
|
HorizontalAlignment = Enum.HorizontalAlignment.Center,
|
||||||
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
||||||
|
}),
|
||||||
|
|
||||||
|
Body = e("Frame", {
|
||||||
|
Size = UDim2.new(0, 360, 1, -32),
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
}, self.props[Roact.Children]),
|
||||||
|
|
||||||
|
Footer = e(RojoFooter),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return Panel
|
||||||
67
plugin/src/Components/RojoFooter.lua
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
local Rojo = script:FindFirstAncestor("Rojo")
|
||||||
|
local Plugin = Rojo.Plugin
|
||||||
|
|
||||||
|
local Roact = require(Rojo.Roact)
|
||||||
|
|
||||||
|
local Config = require(Plugin.Config)
|
||||||
|
local Version = require(Plugin.Version)
|
||||||
|
local Assets = require(Plugin.Assets)
|
||||||
|
local Theme = require(Plugin.Theme)
|
||||||
|
|
||||||
|
local e = Roact.createElement
|
||||||
|
|
||||||
|
local RojoFooter = Roact.Component:extend("RojoFooter")
|
||||||
|
|
||||||
|
function RojoFooter:init()
|
||||||
|
self.footerSize, self.setFooterSize = Roact.createBinding(Vector2.new())
|
||||||
|
self.footerVersionSize, self.setFooterVersionSize = Roact.createBinding(Vector2.new())
|
||||||
|
end
|
||||||
|
|
||||||
|
function RojoFooter:render()
|
||||||
|
return e("Frame", {
|
||||||
|
LayoutOrder = 3,
|
||||||
|
Size = UDim2.new(1, 0, 0, 32),
|
||||||
|
BackgroundColor3 = Theme.SecondaryColor,
|
||||||
|
BorderSizePixel = 0,
|
||||||
|
}, {
|
||||||
|
Padding = e("UIPadding", {
|
||||||
|
PaddingTop = UDim.new(0, 4),
|
||||||
|
PaddingBottom = UDim.new(0, 4),
|
||||||
|
PaddingLeft = UDim.new(0, 8),
|
||||||
|
PaddingRight = UDim.new(0, 8),
|
||||||
|
}),
|
||||||
|
|
||||||
|
LogoContainer = e("Frame", {
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
|
Size = UDim2.new(0, 0, 0, 32),
|
||||||
|
}, {
|
||||||
|
Logo = e("ImageLabel", {
|
||||||
|
Image = Assets.Images.Logo,
|
||||||
|
Size = UDim2.new(0, 80, 0, 40),
|
||||||
|
ScaleType = Enum.ScaleType.Fit,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
Position = UDim2.new(0, 0, 1, -10),
|
||||||
|
AnchorPoint = Vector2.new(0, 1),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
Version = e("TextLabel", {
|
||||||
|
Position = UDim2.new(1, 0, 0, 0),
|
||||||
|
Size = UDim2.new(0, 0, 1, 0),
|
||||||
|
AnchorPoint = Vector2.new(1, 0),
|
||||||
|
Font = Theme.TitleFont,
|
||||||
|
TextSize = 18,
|
||||||
|
Text = Version.display(Config.version),
|
||||||
|
TextXAlignment = Enum.TextXAlignment.Right,
|
||||||
|
TextColor3 = Theme.LightTextColor,
|
||||||
|
BackgroundTransparency = 1,
|
||||||
|
|
||||||
|
[Roact.Change.AbsoluteSize] = function(rbx)
|
||||||
|
self.setFooterVersionSize(rbx.AbsoluteSize)
|
||||||
|
end,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return RojoFooter
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
return {
|
local strict = require(script.Parent.strict)
|
||||||
|
|
||||||
|
local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
|
||||||
|
|
||||||
|
return strict("Config", {
|
||||||
|
isDevBuild = isDevBuild,
|
||||||
codename = "Epiphany",
|
codename = "Epiphany",
|
||||||
version = {0, 5, 0, "-alpha.4"},
|
version = {0, 6, 0, "-alpha.2"},
|
||||||
expectedServerVersionString = "0.5.0 or newer",
|
expectedServerVersionString = "0.6.0 or newer",
|
||||||
protocolVersion = 2,
|
protocolVersion = 3,
|
||||||
defaultHost = "localhost",
|
defaultHost = "localhost",
|
||||||
defaultPort = 34872,
|
defaultPort = 34872,
|
||||||
}
|
})
|
||||||
@@ -1,10 +1,37 @@
|
|||||||
local Config = require(script.Parent.Config)
|
local Config = require(script.Parent.Config)
|
||||||
|
|
||||||
|
local Environment = {
|
||||||
|
User = "User",
|
||||||
|
Dev = "Dev",
|
||||||
|
Test = "Test",
|
||||||
|
}
|
||||||
|
|
||||||
|
local DEFAULT_ENVIRONMENT = Config.isDevBuild and Environment.Dev or Environment.User
|
||||||
|
|
||||||
local VALUES = {
|
local VALUES = {
|
||||||
LogLevel = {
|
LogLevel = {
|
||||||
type = "IntValue",
|
type = "IntValue",
|
||||||
defaultUserValue = 2,
|
values = {
|
||||||
defaultDevValue = 3,
|
[Environment.User] = 2,
|
||||||
|
[Environment.Dev] = 4,
|
||||||
|
[Environment.Test] = 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TypecheckingEnabled = {
|
||||||
|
type = "BoolValue",
|
||||||
|
values = {
|
||||||
|
[Environment.User] = false,
|
||||||
|
[Environment.Dev] = true,
|
||||||
|
[Environment.Test] = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UnstableTwoWaySync = {
|
||||||
|
type = "BoolValue",
|
||||||
|
values = {
|
||||||
|
[Environment.User] = false,
|
||||||
|
[Environment.Dev] = false,
|
||||||
|
[Environment.Test] = false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,6 +43,16 @@ end
|
|||||||
|
|
||||||
local valueContainer = getValueContainer()
|
local valueContainer = getValueContainer()
|
||||||
|
|
||||||
|
game.ChildAdded:Connect(function(child)
|
||||||
|
local success, name = pcall(function()
|
||||||
|
return child.Name
|
||||||
|
end)
|
||||||
|
|
||||||
|
if success and name == CONTAINER_NAME then
|
||||||
|
valueContainer = child
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
local function getStoredValue(name)
|
local function getStoredValue(name)
|
||||||
if valueContainer == nil then
|
if valueContainer == nil then
|
||||||
return nil
|
return nil
|
||||||
@@ -42,7 +79,9 @@ local function setStoredValue(name, kind, value)
|
|||||||
object.Value = value
|
object.Value = value
|
||||||
end
|
end
|
||||||
|
|
||||||
local function createAllValues()
|
local function createAllValues(environment)
|
||||||
|
assert(Environment[environment] ~= nil, "Invalid environment")
|
||||||
|
|
||||||
valueContainer = getValueContainer()
|
valueContainer = getValueContainer()
|
||||||
|
|
||||||
if valueContainer == nil then
|
if valueContainer == nil then
|
||||||
@@ -52,20 +91,61 @@ local function createAllValues()
|
|||||||
end
|
end
|
||||||
|
|
||||||
for name, value in pairs(VALUES) do
|
for name, value in pairs(VALUES) do
|
||||||
setStoredValue(name, value.type, value.defaultDevValue)
|
setStoredValue(name, value.type, value.values[environment])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
_G[("ROJO_%s_DEV_CREATE"):format(Config.codename:upper())] = createAllValues
|
local function getValue(name)
|
||||||
|
assert(VALUES[name] ~= nil, "Invalid DevSettings name")
|
||||||
|
|
||||||
|
local stored = getStoredValue(name)
|
||||||
|
|
||||||
|
if stored ~= nil then
|
||||||
|
return stored
|
||||||
|
end
|
||||||
|
|
||||||
|
return VALUES[name].values[DEFAULT_ENVIRONMENT]
|
||||||
|
end
|
||||||
|
|
||||||
local DevSettings = {}
|
local DevSettings = {}
|
||||||
|
|
||||||
|
function DevSettings:createDevSettings()
|
||||||
|
createAllValues(Environment.Dev)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DevSettings:createTestSettings()
|
||||||
|
createAllValues(Environment.Test)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DevSettings:hasChangedValues()
|
||||||
|
return valueContainer ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function DevSettings:resetValues()
|
||||||
|
if valueContainer then
|
||||||
|
valueContainer:Destroy()
|
||||||
|
valueContainer = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function DevSettings:isEnabled()
|
function DevSettings:isEnabled()
|
||||||
return valueContainer ~= nil
|
return valueContainer ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function DevSettings:getLogLevel()
|
function DevSettings:getLogLevel()
|
||||||
return getStoredValue("LogLevel") or VALUES.LogLevel.defaultUserValue
|
return getValue("LogLevel")
|
||||||
|
end
|
||||||
|
|
||||||
|
function DevSettings:shouldTypecheck()
|
||||||
|
return getValue("TypecheckingEnabled")
|
||||||
|
end
|
||||||
|
|
||||||
|
function DevSettings:twoWaySyncEnabled()
|
||||||
|
return getValue("UnstableTwoWaySync")
|
||||||
|
end
|
||||||
|
|
||||||
|
function _G.ROJO_DEV_CREATE()
|
||||||
|
DevSettings:createDevSettings()
|
||||||
end
|
end
|
||||||
|
|
||||||
return DevSettings
|
return DevSettings
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
local HttpService = game:GetService("HttpService")
|
|
||||||
|
|
||||||
local Promise = require(script.Parent.Parent.Promise)
|
|
||||||
|
|
||||||
local Logging = require(script.Parent.Logging)
|
|
||||||
local HttpError = require(script.Parent.HttpError)
|
|
||||||
local HttpResponse = require(script.Parent.HttpResponse)
|
|
||||||
|
|
||||||
local lastRequestId = 0
|
|
||||||
|
|
||||||
-- TODO: Factor out into separate library, especially error handling
|
|
||||||
local Http = {}
|
|
||||||
|
|
||||||
function Http.get(url)
|
|
||||||
local requestId = lastRequestId + 1
|
|
||||||
lastRequestId = requestId
|
|
||||||
|
|
||||||
Logging.trace("GET(%d) %s", requestId, url)
|
|
||||||
|
|
||||||
return Promise.new(function(resolve, reject)
|
|
||||||
coroutine.wrap(function()
|
|
||||||
local success, response = pcall(function()
|
|
||||||
return HttpService:RequestAsync({
|
|
||||||
Url = url,
|
|
||||||
Method = "GET",
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
if success then
|
|
||||||
Logging.trace("Request %d success: status code %s", requestId, response.StatusCode)
|
|
||||||
resolve(HttpResponse.fromRobloxResponse(response))
|
|
||||||
else
|
|
||||||
Logging.trace("Request %d failure: %s", requestId, response)
|
|
||||||
reject(HttpError.fromErrorString(response))
|
|
||||||
end
|
|
||||||
end)()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Http.post(url, body)
|
|
||||||
local requestId = lastRequestId + 1
|
|
||||||
lastRequestId = requestId
|
|
||||||
|
|
||||||
Logging.trace("POST(%d) %s\n%s", requestId, url, body)
|
|
||||||
|
|
||||||
return Promise.new(function(resolve, reject)
|
|
||||||
coroutine.wrap(function()
|
|
||||||
local success, response = pcall(function()
|
|
||||||
return HttpService:RequestAsync({
|
|
||||||
Url = url,
|
|
||||||
Method = "POST",
|
|
||||||
Body = body,
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
if success then
|
|
||||||
Logging.trace("Request %d success: status code %s", requestId, response.StatusCode)
|
|
||||||
resolve(HttpResponse.fromRobloxResponse(response))
|
|
||||||
else
|
|
||||||
Logging.trace("Request %d failure: %s", requestId, response)
|
|
||||||
reject(HttpError.fromErrorString(response))
|
|
||||||
end
|
|
||||||
end)()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Http.jsonEncode(object)
|
|
||||||
return HttpService:JSONEncode(object)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Http.jsonDecode(source)
|
|
||||||
return HttpService:JSONDecode(source)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Http
|
|
||||||
177
plugin/src/InstanceMap.lua
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
local Log = require(script.Parent.Parent.Log)
|
||||||
|
|
||||||
|
--[[
|
||||||
|
A bidirectional map between instance IDs and Roblox instances. It lets us
|
||||||
|
keep track of every instance we know about.
|
||||||
|
|
||||||
|
TODO: Track ancestry to catch when stuff moves?
|
||||||
|
]]
|
||||||
|
local InstanceMap = {}
|
||||||
|
InstanceMap.__index = InstanceMap
|
||||||
|
|
||||||
|
function InstanceMap.new(onInstanceChanged)
|
||||||
|
local self = {
|
||||||
|
fromIds = {},
|
||||||
|
fromInstances = {},
|
||||||
|
instancesToSignal = {},
|
||||||
|
onInstanceChanged = onInstanceChanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
return setmetatable(self, InstanceMap)
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Disconnect all connections and release all instance references.
|
||||||
|
]]
|
||||||
|
function InstanceMap:stop()
|
||||||
|
-- I think this is safe.
|
||||||
|
for instance in pairs(self.fromInstances) do
|
||||||
|
self:removeInstance(instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:__fmtDebug(output)
|
||||||
|
output:writeLine("InstanceMap {{")
|
||||||
|
output:indent()
|
||||||
|
|
||||||
|
-- Collect all of the entries in the InstanceMap and sort them by their
|
||||||
|
-- label, which helps make our output deterministic.
|
||||||
|
local entries = {}
|
||||||
|
for id, instance in pairs(self.fromIds) do
|
||||||
|
local label = string.format("%s (%s)", instance:GetFullName(), instance.ClassName)
|
||||||
|
|
||||||
|
table.insert(entries, {id, label})
|
||||||
|
end
|
||||||
|
|
||||||
|
table.sort(entries, function(a, b)
|
||||||
|
return a[2] < b[2]
|
||||||
|
end)
|
||||||
|
|
||||||
|
for _, entry in ipairs(entries) do
|
||||||
|
output:writeLine("{}: {}", entry[1], entry[2])
|
||||||
|
end
|
||||||
|
|
||||||
|
output:unindent()
|
||||||
|
output:write("}")
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:insert(id, instance)
|
||||||
|
self.fromIds[id] = instance
|
||||||
|
self.fromInstances[instance] = id
|
||||||
|
self:__connectSignals(instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:removeId(id)
|
||||||
|
local instance = self.fromIds[id]
|
||||||
|
|
||||||
|
if instance ~= nil then
|
||||||
|
self:__disconnectSignals(instance)
|
||||||
|
self.fromIds[id] = nil
|
||||||
|
self.fromInstances[instance] = nil
|
||||||
|
else
|
||||||
|
Log.warn("Attempted to remove nonexistant ID {}", id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:removeInstance(instance)
|
||||||
|
local id = self.fromInstances[instance]
|
||||||
|
self:__disconnectSignals(instance)
|
||||||
|
|
||||||
|
if id ~= nil then
|
||||||
|
self.fromInstances[instance] = nil
|
||||||
|
self.fromIds[id] = nil
|
||||||
|
else
|
||||||
|
Log.warn("Attempted to remove nonexistant instance {}", instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:destroyInstance(instance)
|
||||||
|
local id = self.fromInstances[instance]
|
||||||
|
|
||||||
|
if id ~= nil then
|
||||||
|
self:destroyId(id)
|
||||||
|
else
|
||||||
|
Log.warn("Attempted to destroy untracked instance {}", instance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:destroyId(id)
|
||||||
|
local instance = self.fromIds[id]
|
||||||
|
self:removeId(id)
|
||||||
|
|
||||||
|
if instance ~= nil then
|
||||||
|
local descendantsToDestroy = {}
|
||||||
|
|
||||||
|
for otherInstance in pairs(self.fromInstances) do
|
||||||
|
if otherInstance:IsDescendantOf(instance) then
|
||||||
|
table.insert(descendantsToDestroy, otherInstance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, otherInstance in ipairs(descendantsToDestroy) do
|
||||||
|
self:removeInstance(otherInstance)
|
||||||
|
end
|
||||||
|
|
||||||
|
instance:Destroy()
|
||||||
|
else
|
||||||
|
Log.warn("Attempted to destroy nonexistant ID {}", id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:__connectSignals(instance)
|
||||||
|
-- ValueBase instances have an overriden version of the Changed signal that
|
||||||
|
-- only detects changes to their Value property.
|
||||||
|
--
|
||||||
|
-- We can instead connect listener to each individual property that we care
|
||||||
|
-- about on those objects (Name and Value) to emulate the same idea.
|
||||||
|
if instance:IsA("ValueBase") then
|
||||||
|
local signals = {
|
||||||
|
instance:GetPropertyChangedSignal("Name"):Connect(function()
|
||||||
|
self:__maybeFireInstanceChanged(instance, "Name")
|
||||||
|
end),
|
||||||
|
|
||||||
|
instance:GetPropertyChangedSignal("Value"):Connect(function()
|
||||||
|
self:__maybeFireInstanceChanged(instance, "Value")
|
||||||
|
end),
|
||||||
|
|
||||||
|
instance:GetPropertyChangedSignal("Parent"):Connect(function()
|
||||||
|
self:__maybeFireInstanceChanged(instance, "Parent")
|
||||||
|
end),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.instancesToSignal[instance] = signals
|
||||||
|
else
|
||||||
|
self.instancesToSignal[instance] = instance.Changed:Connect(function(propertyName)
|
||||||
|
self:__maybeFireInstanceChanged(instance, propertyName)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:__maybeFireInstanceChanged(instance, propertyName)
|
||||||
|
Log.trace("{}.{} changed", instance:GetFullName(), propertyName)
|
||||||
|
|
||||||
|
if self.onInstanceChanged ~= nil then
|
||||||
|
self.onInstanceChanged(instance, propertyName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function InstanceMap:__disconnectSignals(instance)
|
||||||
|
local signals = self.instancesToSignal[instance]
|
||||||
|
|
||||||
|
if signals ~= nil then
|
||||||
|
-- In most cases, we only have a single signal, so we avoid keeping
|
||||||
|
-- around the extra table. ValueBase objects force us to use multiple
|
||||||
|
-- signals to emulate the Instance.Changed event, however.
|
||||||
|
if typeof(signals) == "table" then
|
||||||
|
for _, signal in ipairs(signals) do
|
||||||
|
signal:Disconnect()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
signals:Disconnect()
|
||||||
|
end
|
||||||
|
|
||||||
|
self.instancesToSignal[instance] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return InstanceMap
|
||||||
@@ -1,281 +1,401 @@
|
|||||||
local Logging = require(script.Parent.Logging)
|
--[[
|
||||||
|
This module defines the meat of the Rojo plugin and how it manages tracking
|
||||||
|
and mutating the Roblox DOM.
|
||||||
|
]]
|
||||||
|
|
||||||
local function makeInstanceMap()
|
local RbxDom = require(script.Parent.Parent.RbxDom)
|
||||||
local self = {
|
local t = require(script.Parent.Parent.t)
|
||||||
fromIds = {},
|
local Log = require(script.Parent.Parent.Log)
|
||||||
fromInstances = {},
|
|
||||||
}
|
|
||||||
|
|
||||||
function self:insert(id, instance)
|
local Types = require(script.Parent.Types)
|
||||||
self.fromIds[id] = instance
|
local invariant = require(script.Parent.invariant)
|
||||||
self.fromInstances[instance] = id
|
local getCanonicalProperty = require(script.Parent.getCanonicalProperty)
|
||||||
end
|
local setCanonicalProperty = require(script.Parent.setCanonicalProperty)
|
||||||
|
|
||||||
function self:removeId(id)
|
--[[
|
||||||
local instance = self.fromIds[id]
|
This interface represents either a patch created by the hydrate method, or a
|
||||||
|
patch returned from the API.
|
||||||
|
|
||||||
if instance ~= nil then
|
This type should be a subset of Types.ApiInstanceUpdate.
|
||||||
self.fromIds[id] = nil
|
]]
|
||||||
self.fromInstances[instance] = nil
|
local IPatch = t.interface({
|
||||||
else
|
removed = t.array(t.union(Types.RbxId, t.Instance)),
|
||||||
Logging.warn("Attempted to remove nonexistant ID %s", tostring(id))
|
added = t.map(Types.RbxId, Types.ApiInstance),
|
||||||
end
|
updated = t.array(Types.ApiInstanceUpdate),
|
||||||
end
|
})
|
||||||
|
|
||||||
function self:removeInstance(instance)
|
--[[
|
||||||
local id = self.fromInstances[instance]
|
Attempt to safely set the parent of an instance.
|
||||||
|
|
||||||
if id ~= nil then
|
This function will always succeed, even if the actual set failed. This is
|
||||||
self.fromInstances[instance] = nil
|
important for some types like services that will throw even if their current
|
||||||
self.fromIds[id] = nil
|
parent is already set to the requested parent.
|
||||||
else
|
|
||||||
Logging.warn("Attempted to remove nonexistant instance %s", tostring(instance))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function self:destroyId(id)
|
TODO: See if we can eliminate this by being more nuanced with property
|
||||||
local instance = self.fromIds[id]
|
assignment?
|
||||||
self:removeId(id)
|
]]
|
||||||
|
local function safeSetParent(instance, newParent)
|
||||||
if instance ~= nil then
|
pcall(function()
|
||||||
local descendantsToDestroy = {}
|
instance.Parent = newParent
|
||||||
|
end)
|
||||||
for otherInstance in pairs(self.fromInstances) do
|
|
||||||
if otherInstance:IsDescendantOf(instance) then
|
|
||||||
table.insert(descendantsToDestroy, otherInstance)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, otherInstance in ipairs(descendantsToDestroy) do
|
|
||||||
self:removeInstance(otherInstance)
|
|
||||||
end
|
|
||||||
|
|
||||||
instance:Destroy()
|
|
||||||
else
|
|
||||||
Logging.warn("Attempted to destroy nonexistant ID %s", tostring(id))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function setProperty(instance, key, value)
|
--[[
|
||||||
-- The 'Contents' property of LocalizationTable isn't directly exposed, but
|
Similar to setting Parent, some instances really don't like being renamed.
|
||||||
-- has corresponding (deprecated) getters and setters.
|
|
||||||
if key == "Contents" and instance.ClassName == "LocalizationTable" then
|
|
||||||
instance:SetContents(value)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If we don't have permissions to access this value at all, we can skip it.
|
TODO: Should we be throwing away these results or can we be more careful?
|
||||||
local readSuccess, existingValue = pcall(function()
|
]]
|
||||||
return instance[key]
|
local function safeSetName(instance, name)
|
||||||
|
pcall(function()
|
||||||
|
instance.Name = name
|
||||||
end)
|
end)
|
||||||
|
|
||||||
if not readSuccess then
|
|
||||||
-- An error will be thrown if there was a permission issue or if the
|
|
||||||
-- property doesn't exist. In the latter case, we should tell the user
|
|
||||||
-- because it's probably their fault.
|
|
||||||
if existingValue:find("lacking permission") then
|
|
||||||
Logging.trace("Permission error reading property %s on class %s", tostring(key), instance.ClassName)
|
|
||||||
return
|
|
||||||
else
|
|
||||||
error(("Invalid property %s on class %s: %s"):format(tostring(key), instance.ClassName, existingValue), 2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local writeSuccess, err = pcall(function()
|
|
||||||
if existingValue ~= value then
|
|
||||||
instance[key] = value
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
if not writeSuccess then
|
|
||||||
error(("Cannot set property %s on class %s: %s"):format(tostring(key), instance.ClassName, err), 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local Reconciler = {}
|
local Reconciler = {}
|
||||||
Reconciler.__index = Reconciler
|
Reconciler.__index = Reconciler
|
||||||
|
|
||||||
function Reconciler.new()
|
function Reconciler.new(instanceMap)
|
||||||
local self = {
|
local self = {
|
||||||
instanceMap = makeInstanceMap(),
|
-- Tracks all of the instances known by the reconciler by ID.
|
||||||
|
__instanceMap = instanceMap,
|
||||||
}
|
}
|
||||||
|
|
||||||
return setmetatable(self, Reconciler)
|
return setmetatable(self, Reconciler)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Reconciler:applyUpdate(requestedIds, virtualInstancesById)
|
--[[
|
||||||
-- This function may eventually be asynchronous; it will require calls to
|
See Reconciler:__hydrateInternal().
|
||||||
-- the server to resolve instances that don't exist yet.
|
]]
|
||||||
local visitedIds = {}
|
function Reconciler:hydrate(apiInstances, id, instance)
|
||||||
|
local hydratePatch = {
|
||||||
|
removed = {},
|
||||||
|
added = {},
|
||||||
|
updated = {},
|
||||||
|
}
|
||||||
|
|
||||||
for _, id in ipairs(requestedIds) do
|
self:__hydrateInternal(apiInstances, id, instance, hydratePatch)
|
||||||
self:__applyUpdatePiece(id, visitedIds, virtualInstancesById)
|
|
||||||
|
return hydratePatch
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Applies a patch to the Roblox DOM using the reconciler's internal state.
|
||||||
|
|
||||||
|
TODO: This function might only apply some of the patch in the future and
|
||||||
|
require content negotiation with the Rojo server to handle types that aren't
|
||||||
|
editable by scripts.
|
||||||
|
]]
|
||||||
|
local applyPatchSchema = Types.ifEnabled(t.tuple(
|
||||||
|
IPatch
|
||||||
|
))
|
||||||
|
function Reconciler:applyPatch(patch)
|
||||||
|
assert(applyPatchSchema(patch))
|
||||||
|
|
||||||
|
for _, removedIdOrInstance in ipairs(patch.removed) do
|
||||||
|
local removedInstance
|
||||||
|
|
||||||
|
if Types.RbxId(removedIdOrInstance) then
|
||||||
|
-- If this value is an ID, it's assumed to be an instance that the
|
||||||
|
-- Rojo server knows about.
|
||||||
|
removedInstance = self.__instanceMap.fromIds[removedIdOrInstance]
|
||||||
|
self.__instanceMap:removeId(removedIdOrInstance)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If this entry was an ID that we didn't know about, removedInstance
|
||||||
|
-- will be nil, which we guard against in case of minor tree desync.
|
||||||
|
if removedInstance ~= nil then
|
||||||
|
-- Ensure that if any descendants are tracked by Rojo, that we
|
||||||
|
-- properly un-track them.
|
||||||
|
for _, descendantInstance in ipairs(removedInstance:GetDescendants()) do
|
||||||
|
self.__instanceMap:removeInstance(descendantInstance)
|
||||||
|
end
|
||||||
|
|
||||||
|
removedInstance:Destroy()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: This loop assumes that apiInstance.ParentId is never nil. The Rojo
|
||||||
|
-- plugin can't create a new top-level DataModel anyways, so this should
|
||||||
|
-- only be violated in cases that are already erroneous.
|
||||||
|
for id, apiInstance in pairs(patch.added) do
|
||||||
|
if self.__instanceMap.fromIds[id] == nil then
|
||||||
|
-- Find the first ancestor of this instance that is marked for an
|
||||||
|
-- addition.
|
||||||
|
--
|
||||||
|
-- This helps us make sure we only reify each instance once, and we
|
||||||
|
-- start from the top.
|
||||||
|
while patch.added[apiInstance.Parent] ~= nil do
|
||||||
|
id = apiInstance.Parent
|
||||||
|
apiInstance = patch.added[id]
|
||||||
|
end
|
||||||
|
|
||||||
|
local parentInstance = self.__instanceMap.fromIds[apiInstance.Parent]
|
||||||
|
|
||||||
|
if parentInstance == nil then
|
||||||
|
invariant(
|
||||||
|
"Cannot add an instance from a patch that has no parent.\nInstance {} with parent {}.\nState: {:#?}",
|
||||||
|
id,
|
||||||
|
apiInstance.Parent,
|
||||||
|
self.__instanceMap
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
self:__reifyInstance(patch.added, id, parentInstance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, update in ipairs(patch.updated) do
|
||||||
|
local instance = self.__instanceMap.fromIds[update.id]
|
||||||
|
|
||||||
|
if instance == nil then
|
||||||
|
invariant(
|
||||||
|
"Cannot update an instance that does not exist in the reconciler's state.\nInstance {}\nState: {:#?}",
|
||||||
|
update.id,
|
||||||
|
self.__instanceMap
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
if update.changedClassName ~= nil then
|
||||||
|
error("TODO: Support changing class name by destroying + recreating instance.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if update.changedName ~= nil then
|
||||||
|
instance.Name = update.changedName
|
||||||
|
end
|
||||||
|
|
||||||
|
if update.changedMetadata ~= nil then
|
||||||
|
print("TODO: Support changing metadata, if necessary.")
|
||||||
|
end
|
||||||
|
|
||||||
|
if update.changedProperties ~= nil then
|
||||||
|
for propertyName, propertyValue in pairs(update.changedProperties) do
|
||||||
|
-- TODO: Gracefully handle this error instead?
|
||||||
|
assert(setCanonicalProperty(instance, propertyName, self:__decodeApiValue(propertyValue)))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--[[
|
--[[
|
||||||
Update an existing instance, including its properties and children, to match
|
Transforms a value into one that can be sent over the network back to the
|
||||||
the given information.
|
Rojo server.
|
||||||
]]
|
|
||||||
function Reconciler:reconcile(virtualInstancesById, id, instance)
|
|
||||||
local virtualInstance = virtualInstancesById[id]
|
|
||||||
|
|
||||||
-- If an instance changes ClassName, we assume it's very different. That's
|
This operation can fail, and so it returns bool, value.
|
||||||
-- not always the case!
|
]]
|
||||||
if virtualInstance.ClassName ~= instance.ClassName then
|
function Reconciler:encodeApiValue(value)
|
||||||
-- TODO: Preserve existing children instead?
|
if typeof(value) == "string" then
|
||||||
local parent = instance.Parent
|
return true, {
|
||||||
self.instanceMap:destroyId(id)
|
Type = "String",
|
||||||
return self:__reify(virtualInstancesById, id, parent)
|
Value = value,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
self.instanceMap:insert(id, instance)
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
-- Some instances don't like being named, even if their name already matches
|
--[[
|
||||||
setProperty(instance, "Name", virtualInstance.Name)
|
Transforms a value encoded by rbx_dom_weak on the server side into a value
|
||||||
|
usable by Rojo's reconciler, potentially using RbxDom.
|
||||||
|
]]
|
||||||
|
function Reconciler:__decodeApiValue(apiValue)
|
||||||
|
assert(Types.ApiValue(apiValue))
|
||||||
|
|
||||||
for key, value in pairs(virtualInstance.Properties) do
|
-- Refs are represented as IDs in the same space that Rojo's protocol uses.
|
||||||
setProperty(instance, key, value.Value)
|
if apiValue.Type == "Ref" then
|
||||||
|
-- TODO: This ref could be pointing at an instance we haven't created
|
||||||
|
-- yet!
|
||||||
|
|
||||||
|
return self.__instanceMap.fromIds[apiValue.Value]
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, decodedValue = RbxDom.EncodedValue.decode(apiValue)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
error(decodedValue, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return decodedValue
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Constructs an instance from an ApiInstance without any of its children.
|
||||||
|
]]
|
||||||
|
local reifySingleInstanceSchema = Types.ifEnabled(t.tuple(
|
||||||
|
Types.ApiInstance
|
||||||
|
))
|
||||||
|
function Reconciler:__reifySingleInstance(apiInstance)
|
||||||
|
assert(reifySingleInstanceSchema(apiInstance))
|
||||||
|
|
||||||
|
-- Instance.new can fail if we're passing in something that can't be
|
||||||
|
-- created, like a service, something enabled with a feature flag, or
|
||||||
|
-- something that requires higher security than we have.
|
||||||
|
local ok, instance = pcall(Instance.new, apiInstance.ClassName)
|
||||||
|
if not ok then
|
||||||
|
return false, instance
|
||||||
|
end
|
||||||
|
|
||||||
|
-- TODO: When can setting Name fail here?
|
||||||
|
safeSetName(instance, apiInstance.Name)
|
||||||
|
|
||||||
|
for key, value in pairs(apiInstance.Properties) do
|
||||||
|
setCanonicalProperty(instance, key, self:__decodeApiValue(value))
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, instance
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Construct an instance and all of its descendants, parent it to the given
|
||||||
|
instance, and insert it into the reconciler's internal state.
|
||||||
|
]]
|
||||||
|
local reifyInstanceSchema = Types.ifEnabled(t.tuple(
|
||||||
|
t.map(Types.RbxId, Types.VirtualInstance),
|
||||||
|
Types.RbxId,
|
||||||
|
t.Instance
|
||||||
|
))
|
||||||
|
function Reconciler:__reifyInstance(apiInstances, id, parentInstance)
|
||||||
|
assert(reifyInstanceSchema(apiInstances, id, parentInstance))
|
||||||
|
|
||||||
|
local apiInstance = apiInstances[id]
|
||||||
|
local ok, instance = self:__reifySingleInstance(apiInstance)
|
||||||
|
|
||||||
|
-- TODO: Propagate this error upward to handle it elsewhere?
|
||||||
|
if not ok then
|
||||||
|
error(("Couldn't create an instance of type %q, a child of %s"):format(
|
||||||
|
apiInstance.ClassName,
|
||||||
|
parentInstance:GetFullName()
|
||||||
|
))
|
||||||
|
end
|
||||||
|
|
||||||
|
self.__instanceMap:insert(id, instance)
|
||||||
|
|
||||||
|
for _, childId in ipairs(apiInstance.Children) do
|
||||||
|
self:__reifyInstance(apiInstances, childId, instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
safeSetParent(instance, parentInstance)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
end
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Populates the reconciler's internal state, maps IDs to instances that the
|
||||||
|
Rojo plugin knows about, and generates a patch that would update the Roblox
|
||||||
|
tree to match Rojo's view of the tree.
|
||||||
|
]]
|
||||||
|
local hydrateSchema = Types.ifEnabled(t.tuple(
|
||||||
|
t.map(Types.RbxId, Types.VirtualInstance),
|
||||||
|
Types.RbxId,
|
||||||
|
t.Instance,
|
||||||
|
IPatch
|
||||||
|
))
|
||||||
|
function Reconciler:__hydrateInternal(apiInstances, id, instance, hydratePatch)
|
||||||
|
assert(hydrateSchema(apiInstances, id, instance, hydratePatch))
|
||||||
|
|
||||||
|
self.__instanceMap:insert(id, instance)
|
||||||
|
|
||||||
|
local apiInstance = apiInstances[id]
|
||||||
|
|
||||||
|
local function markIdAdded(id)
|
||||||
|
local apiInstance = apiInstances[id]
|
||||||
|
hydratePatch.added[id] = apiInstance
|
||||||
|
|
||||||
|
for _, childId in ipairs(apiInstance.Children) do
|
||||||
|
markIdAdded(childId)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local changedName = nil
|
||||||
|
local changedProperties = {}
|
||||||
|
|
||||||
|
if apiInstance.Name ~= instance.Name then
|
||||||
|
changedName = apiInstance.Name
|
||||||
|
end
|
||||||
|
|
||||||
|
for propertyName, virtualValue in pairs(apiInstance.Properties) do
|
||||||
|
local success, existingValue = getCanonicalProperty(instance, propertyName)
|
||||||
|
|
||||||
|
if success then
|
||||||
|
local decodedValue = self:__decodeApiValue(virtualValue)
|
||||||
|
|
||||||
|
if existingValue ~= decodedValue then
|
||||||
|
changedProperties[propertyName] = virtualValue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If any properties differed from the virtual instance we read, add it to
|
||||||
|
-- the hydrate patch so that we can catch up.
|
||||||
|
if changedName ~= nil or next(changedProperties) ~= nil then
|
||||||
|
table.insert(hydratePatch.updated, {
|
||||||
|
id = id,
|
||||||
|
changedName = changedName,
|
||||||
|
changedClassName = nil,
|
||||||
|
changedProperties = changedProperties,
|
||||||
|
changedMetadata = nil,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local existingChildren = instance:GetChildren()
|
local existingChildren = instance:GetChildren()
|
||||||
|
|
||||||
local unvisitedExistingChildren = {}
|
-- For each existing child, we'll track whether it's been paired with an
|
||||||
for _, child in ipairs(existingChildren) do
|
-- instance that the Rojo server knows about.
|
||||||
unvisitedExistingChildren[child] = true
|
local isExistingChildVisited = {}
|
||||||
|
for i = 1, #existingChildren do
|
||||||
|
isExistingChildVisited[i] = false
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, childId in ipairs(virtualInstance.Children) do
|
for _, childId in ipairs(apiInstance.Children) do
|
||||||
local childData = virtualInstancesById[childId]
|
local apiChild = apiInstances[childId]
|
||||||
|
|
||||||
local existingChildInstance
|
local childInstance
|
||||||
for instance in pairs(unvisitedExistingChildren) do
|
|
||||||
local ok, name, className = pcall(function()
|
|
||||||
return instance.Name, instance.ClassName
|
|
||||||
end)
|
|
||||||
|
|
||||||
if ok then
|
for childIndex, instance in ipairs(existingChildren) do
|
||||||
if name == childData.Name and className == childData.ClassName then
|
if not isExistingChildVisited[childIndex] then
|
||||||
existingChildInstance = instance
|
-- We guard accessing Name and ClassName in order to avoid
|
||||||
|
-- tripping over children of DataModel that Rojo won't have
|
||||||
|
-- permissions to access at all.
|
||||||
|
local ok, name, className = pcall(function()
|
||||||
|
return instance.Name, instance.ClassName
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- This rule is very conservative and could be loosened in the
|
||||||
|
-- future, or more heuristics could be introduced.
|
||||||
|
if ok and name == apiChild.Name and className == apiChild.ClassName then
|
||||||
|
childInstance = instance
|
||||||
|
isExistingChildVisited[childIndex] = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if existingChildInstance ~= nil then
|
if childInstance ~= nil then
|
||||||
unvisitedExistingChildren[existingChildInstance] = nil
|
-- We found an instance that matches the instance from the API, yay!
|
||||||
self:reconcile(virtualInstancesById, childId, existingChildInstance)
|
self:__hydrateInternal(apiInstances, childId, childInstance, hydratePatch)
|
||||||
else
|
else
|
||||||
self:__reify(virtualInstancesById, childId, instance)
|
markIdAdded(childId)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if self:__shouldClearUnknownInstances(virtualInstance) then
|
-- Any unvisited children at this point aren't known by Rojo and we can
|
||||||
for existingChildInstance in pairs(unvisitedExistingChildren) do
|
-- destroy them unless the user has explicitly asked us to preserve children
|
||||||
self.instanceMap:removeInstance(existingChildInstance)
|
-- of this instance.
|
||||||
existingChildInstance:Destroy()
|
local shouldClearUnknown = self:__shouldClearUnknownChildren(apiInstance)
|
||||||
|
if shouldClearUnknown then
|
||||||
|
for childIndex, visited in ipairs(isExistingChildVisited) do
|
||||||
|
if not visited then
|
||||||
|
table.insert(hydratePatch.removed, existingChildren[childIndex])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- The root instance of a project won't have a parent, like the DataModel,
|
|
||||||
-- so we need to be careful here.
|
|
||||||
if virtualInstance.Parent ~= nil then
|
|
||||||
local parent = self.instanceMap.fromIds[virtualInstance.Parent]
|
|
||||||
|
|
||||||
if parent == nil then
|
|
||||||
Logging.info("Instance %s wanted parent of %s", tostring(id), tostring(virtualInstance.Parent))
|
|
||||||
error("Rojo bug: During reconciliation, an instance referred to an instance ID as parent that does not exist.")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Some instances, like services, don't like having their Parent
|
|
||||||
-- property poked, even if we're setting it to the same value.
|
|
||||||
setProperty(instance, "Parent", parent)
|
|
||||||
if instance.Parent ~= parent then
|
|
||||||
instance.Parent = parent
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return instance
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Reconciler:__shouldClearUnknownInstances(virtualInstance)
|
function Reconciler:__shouldClearUnknownChildren(apiInstance)
|
||||||
if virtualInstance.Metadata ~= nil then
|
if apiInstance.Metadata ~= nil then
|
||||||
return not virtualInstance.Metadata.ignoreUnknownInstances
|
return not apiInstance.Metadata.ignoreUnknownInstances
|
||||||
else
|
else
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Reconciler:__reify(virtualInstancesById, id, parent)
|
|
||||||
local virtualInstance = virtualInstancesById[id]
|
|
||||||
|
|
||||||
local instance = Instance.new(virtualInstance.ClassName)
|
|
||||||
|
|
||||||
for key, value in pairs(virtualInstance.Properties) do
|
|
||||||
-- TODO: Branch on value.Type
|
|
||||||
setProperty(instance, key, value.Value)
|
|
||||||
end
|
|
||||||
|
|
||||||
instance.Name = virtualInstance.Name
|
|
||||||
|
|
||||||
for _, childId in ipairs(virtualInstance.Children) do
|
|
||||||
self:__reify(virtualInstancesById, childId, instance)
|
|
||||||
end
|
|
||||||
|
|
||||||
setProperty(instance, "Parent", parent)
|
|
||||||
self.instanceMap:insert(id, instance)
|
|
||||||
|
|
||||||
return instance
|
|
||||||
end
|
|
||||||
|
|
||||||
function Reconciler:__applyUpdatePiece(id, visitedIds, virtualInstancesById)
|
|
||||||
if visitedIds[id] then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
visitedIds[id] = true
|
|
||||||
|
|
||||||
local virtualInstance = virtualInstancesById[id]
|
|
||||||
local instance = self.instanceMap.fromIds[id]
|
|
||||||
|
|
||||||
-- The instance was deleted in this update
|
|
||||||
if virtualInstance == nil then
|
|
||||||
self.instanceMap:destroyId(id)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- An instance we know about was updated
|
|
||||||
if instance ~= nil then
|
|
||||||
self:reconcile(virtualInstancesById, id, instance)
|
|
||||||
return instance
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If the instance's parent already exists, we can stick it there
|
|
||||||
local parentInstance = self.instanceMap.fromIds[virtualInstance.Parent]
|
|
||||||
if parentInstance ~= nil then
|
|
||||||
self:__reify(virtualInstancesById, id, parentInstance)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Otherwise, we can check if this response payload contained the parent and
|
|
||||||
-- work from there instead.
|
|
||||||
local parentData = virtualInstancesById[virtualInstance.Parent]
|
|
||||||
if parentData ~= nil then
|
|
||||||
if visitedIds[virtualInstance.Parent] then
|
|
||||||
error("Rojo bug: An instance was present and marked as visited but its instance was missing")
|
|
||||||
end
|
|
||||||
|
|
||||||
self:__applyUpdatePiece(virtualInstance.Parent, visitedIds, virtualInstancesById)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
Logging.trace("Instance ID %s, parent ID %s", tostring(id), tostring(virtualInstance.Parent))
|
|
||||||
error("Rojo NYI: Instances with parents that weren't mentioned in an update payload")
|
|
||||||
end
|
|
||||||
|
|
||||||
return Reconciler
|
return Reconciler
|
||||||
213
plugin/src/ServeSession.lua
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
local Log = require(script.Parent.Parent.Log)
|
||||||
|
local Fmt = require(script.Parent.Parent.Fmt)
|
||||||
|
local t = require(script.Parent.Parent.t)
|
||||||
|
|
||||||
|
local DevSettings = require(script.Parent.DevSettings)
|
||||||
|
local InstanceMap = require(script.Parent.InstanceMap)
|
||||||
|
local Reconciler = require(script.Parent.Reconciler)
|
||||||
|
local strict = require(script.Parent.strict)
|
||||||
|
|
||||||
|
local Status = strict("Session.Status", {
|
||||||
|
NotStarted = "NotStarted",
|
||||||
|
Connecting = "Connecting",
|
||||||
|
Connected = "Connected",
|
||||||
|
Disconnected = "Disconnected",
|
||||||
|
})
|
||||||
|
|
||||||
|
local function debugPatch(patch)
|
||||||
|
return Fmt.debugify(patch, function(patch, output)
|
||||||
|
output:writeLine("Patch {{")
|
||||||
|
output:indent()
|
||||||
|
|
||||||
|
for removed in ipairs(patch.removed) do
|
||||||
|
output:writeLine("Remove ID {}", removed)
|
||||||
|
end
|
||||||
|
|
||||||
|
for id, added in pairs(patch.added) do
|
||||||
|
output:writeLine("Add ID {} {:#?}", id, added)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, updated in ipairs(patch.updated) do
|
||||||
|
output:writeLine("Update ID {} {:#?}", updated.id, updated)
|
||||||
|
end
|
||||||
|
|
||||||
|
output:unindent()
|
||||||
|
output:write("}")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
local ServeSession = {}
|
||||||
|
ServeSession.__index = ServeSession
|
||||||
|
|
||||||
|
ServeSession.Status = Status
|
||||||
|
|
||||||
|
local validateServeOptions = t.strictInterface({
|
||||||
|
apiContext = t.table,
|
||||||
|
})
|
||||||
|
|
||||||
|
function ServeSession.new(options)
|
||||||
|
assert(validateServeOptions(options))
|
||||||
|
|
||||||
|
-- Declare self ahead of time to capture it in a closure
|
||||||
|
local self
|
||||||
|
local function onInstanceChanged(instance, propertyName)
|
||||||
|
self:__onInstanceChanged(instance, propertyName)
|
||||||
|
end
|
||||||
|
|
||||||
|
local instanceMap = InstanceMap.new(onInstanceChanged)
|
||||||
|
local reconciler = Reconciler.new(instanceMap)
|
||||||
|
|
||||||
|
self = {
|
||||||
|
__status = Status.NotStarted,
|
||||||
|
__apiContext = options.apiContext,
|
||||||
|
__reconciler = reconciler,
|
||||||
|
__instanceMap = instanceMap,
|
||||||
|
__statusChangedCallback = nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
setmetatable(self, ServeSession)
|
||||||
|
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:__fmtDebug(output)
|
||||||
|
output:writeLine("ServeSession {{")
|
||||||
|
output:indent()
|
||||||
|
|
||||||
|
output:writeLine("API Context: {:#?}", self.__apiContext)
|
||||||
|
output:writeLine("Instances: {:#?}", self.__instanceMap)
|
||||||
|
|
||||||
|
output:unindent()
|
||||||
|
output:write("}")
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:onStatusChanged(callback)
|
||||||
|
self.__statusChangedCallback = callback
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:start()
|
||||||
|
self:__setStatus(Status.Connecting)
|
||||||
|
|
||||||
|
self.__apiContext:connect()
|
||||||
|
:andThen(function(serverInfo)
|
||||||
|
self:__setStatus(Status.Connected)
|
||||||
|
|
||||||
|
local rootInstanceId = serverInfo.rootInstanceId
|
||||||
|
|
||||||
|
return self:__initialSync(rootInstanceId)
|
||||||
|
:andThen(function()
|
||||||
|
return self:__mainSyncLoop()
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
:catch(function(err)
|
||||||
|
self:__stopInternal(err)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:stop()
|
||||||
|
self:__stopInternal()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:__onInstanceChanged(instance, propertyName)
|
||||||
|
if not DevSettings:twoWaySyncEnabled() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local instanceId = self.__instanceMap.fromInstances[instance]
|
||||||
|
|
||||||
|
if instanceId == nil then
|
||||||
|
Log.warn("Ignoring change for instance {:?} as it is unknown to Rojo", instance)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local remove = nil
|
||||||
|
|
||||||
|
local update = {
|
||||||
|
id = instanceId,
|
||||||
|
changedProperties = {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if propertyName == "Name" then
|
||||||
|
update.changedName = instance.Name
|
||||||
|
elseif propertyName == "Parent" then
|
||||||
|
if instance.Parent == nil then
|
||||||
|
update = nil
|
||||||
|
remove = instanceId
|
||||||
|
else
|
||||||
|
Log.warn("Cannot sync non-nil Parent property changes yet")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local success, encoded = self.__reconciler:encodeApiValue(instance[propertyName])
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
Log.warn("Could not sync back property {:?}.{}", instance, propertyName)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
update.changedProperties[propertyName] = encoded
|
||||||
|
end
|
||||||
|
|
||||||
|
local patch = {
|
||||||
|
removed = {remove},
|
||||||
|
added = {},
|
||||||
|
updated = {update},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.__apiContext:write(patch)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:__initialSync(rootInstanceId)
|
||||||
|
return self.__apiContext:read({ rootInstanceId })
|
||||||
|
:andThen(function(readResponseBody)
|
||||||
|
-- Tell the API Context that we're up-to-date with the version of
|
||||||
|
-- the tree defined in this response.
|
||||||
|
self.__apiContext:setMessageCursor(readResponseBody.messageCursor)
|
||||||
|
|
||||||
|
Log.trace("Computing changes that plugin needs to make to catch up to server...")
|
||||||
|
|
||||||
|
-- Calculate the initial patch to apply to the DataModel to catch us
|
||||||
|
-- up to what Rojo thinks the place should look like.
|
||||||
|
local hydratePatch = self.__reconciler:hydrate(
|
||||||
|
readResponseBody.instances,
|
||||||
|
rootInstanceId,
|
||||||
|
game
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.trace("Computed hydration patch: {:#?}", debugPatch(hydratePatch))
|
||||||
|
|
||||||
|
-- TODO: Prompt user to notify them of this patch, since it's
|
||||||
|
-- effectively a conflict between the Rojo server and the client.
|
||||||
|
|
||||||
|
self.__reconciler:applyPatch(hydratePatch)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:__mainSyncLoop()
|
||||||
|
return self.__apiContext:retrieveMessages()
|
||||||
|
:andThen(function(messages)
|
||||||
|
for _, message in ipairs(messages) do
|
||||||
|
self.__reconciler:applyPatch(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.__status ~= Status.Disconnected then
|
||||||
|
return self:__mainSyncLoop()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:__stopInternal(err)
|
||||||
|
self:__setStatus(Status.Disconnected, err)
|
||||||
|
self.__apiContext:disconnect()
|
||||||
|
self.__instanceMap:stop()
|
||||||
|
end
|
||||||
|
|
||||||
|
function ServeSession:__setStatus(status, detail)
|
||||||
|
self.__status = status
|
||||||
|
|
||||||
|
if self.__statusChangedCallback ~= nil then
|
||||||
|
self.__statusChangedCallback(status, detail)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return ServeSession
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
local Rojo = script:FindFirstAncestor("Rojo")
|
|
||||||
|
|
||||||
local Promise = require(Rojo.Promise)
|
|
||||||
|
|
||||||
local ApiContext = require(script.Parent.ApiContext)
|
|
||||||
local Reconciler = require(script.Parent.Reconciler)
|
|
||||||
|
|
||||||
local Session = {}
|
|
||||||
Session.__index = Session
|
|
||||||
|
|
||||||
function Session.new(config)
|
|
||||||
local remoteUrl = ("http://%s:%s"):format(config.address, config.port)
|
|
||||||
local api = ApiContext.new(remoteUrl)
|
|
||||||
|
|
||||||
local self = {
|
|
||||||
onError = config.onError,
|
|
||||||
disconnected = false,
|
|
||||||
reconciler = Reconciler.new(),
|
|
||||||
api = api,
|
|
||||||
}
|
|
||||||
|
|
||||||
api:connect()
|
|
||||||
:andThen(function()
|
|
||||||
if self.disconnected then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return api:read({api.rootInstanceId})
|
|
||||||
end)
|
|
||||||
:andThen(function(response)
|
|
||||||
if self.disconnected then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
self.reconciler:reconcile(response.instances, api.rootInstanceId, game)
|
|
||||||
return self:__processMessages()
|
|
||||||
end)
|
|
||||||
:catch(function(message)
|
|
||||||
self.disconnected = true
|
|
||||||
self.onError(message)
|
|
||||||
end)
|
|
||||||
|
|
||||||
return not self.disconnected, setmetatable(self, Session)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Session:__processMessages()
|
|
||||||
if self.disconnected then
|
|
||||||
return Promise.resolve()
|
|
||||||
end
|
|
||||||
|
|
||||||
return self.api:retrieveMessages()
|
|
||||||
:andThen(function(messages)
|
|
||||||
local promise = Promise.resolve(nil)
|
|
||||||
|
|
||||||
for _, message in ipairs(messages) do
|
|
||||||
promise = promise:andThen(function()
|
|
||||||
return self:__onMessage(message)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return promise
|
|
||||||
end)
|
|
||||||
:andThen(function()
|
|
||||||
return self:__processMessages()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Session:__onMessage(message)
|
|
||||||
if self.disconnected then
|
|
||||||
return Promise.resolve()
|
|
||||||
end
|
|
||||||
|
|
||||||
local requestedIds = {}
|
|
||||||
|
|
||||||
for _, id in ipairs(message.added) do
|
|
||||||
table.insert(requestedIds, id)
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, id in ipairs(message.updated) do
|
|
||||||
table.insert(requestedIds, id)
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, id in ipairs(message.removed) do
|
|
||||||
table.insert(requestedIds, id)
|
|
||||||
end
|
|
||||||
|
|
||||||
return self.api:read(requestedIds)
|
|
||||||
:andThen(function(response)
|
|
||||||
return self.reconciler:applyUpdate(requestedIds, response.instances)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Session:disconnect()
|
|
||||||
self.disconnected = true
|
|
||||||
end
|
|
||||||
|
|
||||||
return Session
|
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
local Theme = {
|
local strict = require(script.Parent.strict)
|
||||||
|
|
||||||
|
return strict("Theme", {
|
||||||
ButtonFont = Enum.Font.GothamSemibold,
|
ButtonFont = Enum.Font.GothamSemibold,
|
||||||
InputFont = Enum.Font.Code,
|
InputFont = Enum.Font.Code,
|
||||||
TitleFont = Enum.Font.GothamBold,
|
TitleFont = Enum.Font.GothamBold,
|
||||||
MainFont = Enum.Font.Gotham,
|
MainFont = Enum.Font.Gotham,
|
||||||
|
|
||||||
AccentColor = Color3.fromRGB(136, 0, 27),
|
AccentColor = Color3.fromRGB(225, 56, 53),
|
||||||
AccentLightColor = Color3.fromRGB(210, 145, 157),
|
AccentLightColor = Color3.fromRGB(255, 146, 145),
|
||||||
PrimaryColor = Color3.fromRGB(20, 20, 20),
|
PrimaryColor = Color3.fromRGB(64, 64, 64),
|
||||||
SecondaryColor = Color3.fromRGB(235, 235, 235),
|
SecondaryColor = Color3.fromRGB(235, 235, 235),
|
||||||
LightTextColor = Color3.fromRGB(140, 140, 140),
|
LightTextColor = Color3.fromRGB(160, 160, 160),
|
||||||
}
|
})
|
||||||
|
|
||||||
setmetatable(Theme, {
|
|
||||||
__index = function(_, key)
|
|
||||||
error(("%s is not a valid member of Theme"):format(key), 2)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
return Theme
|
|
||||||
99
plugin/src/Types.lua
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
local t = require(script.Parent.Parent.t)
|
||||||
|
|
||||||
|
local DevSettings = require(script.Parent.DevSettings)
|
||||||
|
local strict = require(script.Parent.strict)
|
||||||
|
|
||||||
|
local RbxId = t.string
|
||||||
|
|
||||||
|
local ApiValue = t.interface({
|
||||||
|
Type = t.string,
|
||||||
|
Value = t.optional(t.any),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ApiInstanceMetadata = t.interface({
|
||||||
|
ignoreUnknownInstances = t.optional(t.boolean),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ApiInstance = t.interface({
|
||||||
|
Id = RbxId,
|
||||||
|
Parent = t.optional(RbxId),
|
||||||
|
Name = t.string,
|
||||||
|
ClassName = t.string,
|
||||||
|
Properties = t.map(t.string, ApiValue),
|
||||||
|
Metadata = t.optional(ApiInstanceMetadata),
|
||||||
|
Children = t.array(RbxId),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ApiInstanceUpdate = t.interface({
|
||||||
|
id = RbxId,
|
||||||
|
changedName = t.optional(t.string),
|
||||||
|
changedClassName = t.optional(t.string),
|
||||||
|
changedProperties = t.map(t.string, ApiValue),
|
||||||
|
changedMetadata = t.optional(ApiInstanceMetadata),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ApiSubscribeMessage = t.interface({
|
||||||
|
removed = t.array(RbxId),
|
||||||
|
added = t.map(RbxId, ApiInstance),
|
||||||
|
updated = t.array(ApiInstanceUpdate),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ApiInfoResponse = t.interface({
|
||||||
|
sessionId = t.string,
|
||||||
|
serverVersion = t.string,
|
||||||
|
protocolVersion = t.number,
|
||||||
|
expectedPlaceIds = t.optional(t.array(t.number)),
|
||||||
|
rootInstanceId = RbxId,
|
||||||
|
})
|
||||||
|
|
||||||
|
local ApiReadResponse = t.interface({
|
||||||
|
sessionId = t.string,
|
||||||
|
messageCursor = t.number,
|
||||||
|
instances = t.map(RbxId, ApiInstance),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ApiSubscribeResponse = t.interface({
|
||||||
|
sessionId = t.string,
|
||||||
|
messageCursor = t.number,
|
||||||
|
messages = t.array(ApiSubscribeMessage),
|
||||||
|
})
|
||||||
|
|
||||||
|
local ApiError = t.interface({
|
||||||
|
kind = t.union(
|
||||||
|
t.literal("NotFound"),
|
||||||
|
t.literal("BadRequest"),
|
||||||
|
t.literal("InternalError")
|
||||||
|
),
|
||||||
|
details = t.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
local function ifEnabled(innerCheck)
|
||||||
|
return function(...)
|
||||||
|
if DevSettings:shouldTypecheck() then
|
||||||
|
return innerCheck(...)
|
||||||
|
else
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return strict("Types", {
|
||||||
|
ifEnabled = ifEnabled,
|
||||||
|
|
||||||
|
ApiInfoResponse = ApiInfoResponse,
|
||||||
|
ApiReadResponse = ApiReadResponse,
|
||||||
|
ApiSubscribeResponse = ApiSubscribeResponse,
|
||||||
|
ApiError = ApiError,
|
||||||
|
|
||||||
|
ApiInstance = ApiInstance,
|
||||||
|
ApiInstanceUpdate = ApiInstanceUpdate,
|
||||||
|
ApiInstanceMetadata = ApiInstanceMetadata,
|
||||||
|
ApiSubscribeMessage = ApiSubscribeMessage,
|
||||||
|
ApiValue = ApiValue,
|
||||||
|
RbxId = RbxId,
|
||||||
|
|
||||||
|
-- Deprecated aliases during transition
|
||||||
|
VirtualInstance = ApiInstance,
|
||||||
|
VirtualMetadata = ApiInstanceMetadata,
|
||||||
|
VirtualValue = ApiValue,
|
||||||
|
})
|
||||||
39
plugin/src/getCanonicalProperty.lua
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
local RbxDom = require(script.Parent.Parent.RbxDom)
|
||||||
|
|
||||||
|
--[[
|
||||||
|
Attempts to set a property on the given instance.
|
||||||
|
]]
|
||||||
|
local function getCanonincalProperty(instance, propertyName)
|
||||||
|
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
|
||||||
|
|
||||||
|
-- We can skip unknown properties; they're not likely reflected to Lua.
|
||||||
|
--
|
||||||
|
-- A good example of a property like this is `Model.ModelInPrimary`, which
|
||||||
|
-- is serialized but not reflected to Lua.
|
||||||
|
if descriptor == nil then
|
||||||
|
return false, "unknown property"
|
||||||
|
end
|
||||||
|
|
||||||
|
if descriptor.scriptability == "None" or descriptor.scriptability == "Write" then
|
||||||
|
return false, "unreadable property"
|
||||||
|
end
|
||||||
|
|
||||||
|
local success, valueOrErr = descriptor:read(instance)
|
||||||
|
|
||||||
|
if not success then
|
||||||
|
local err = valueOrErr
|
||||||
|
|
||||||
|
-- If we don't have permission to read a property, we can chalk that up
|
||||||
|
-- to our database being out of date and the engine being right.
|
||||||
|
if err.kind == RbxDom.Error.Kind.Roblox and err.extra:find("lacking permission") then
|
||||||
|
return false, "permission error"
|
||||||
|
end
|
||||||
|
|
||||||
|
local message = ("Invalid property %s.%s: %s"):format(descriptor.className, descriptor.name, tostring(err))
|
||||||
|
error(message, 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return true, valueOrErr
|
||||||
|
end
|
||||||
|
|
||||||
|
return getCanonincalProperty
|
||||||